@metatrongg/sdk
The universal typed client for the Metatron API. It wraps every route the api exposes -- OAuth 2.1, payments (charge
/ status / limits / price / intent / payout / distribute), the realtime sockets, webhook verification, and the full
first-party surface (profile, wallets, messaging, notifications, social graph, payment dashboard, catalog, developer
console, admin) -- so any consumer, first-party or third-party, calls it instead of hand-rolling fetch/WS plumbing.
Install
npm i @metatrongg/sdk # or: pnpm add @metatrongg/sdk / yarn add @metatrongg/sdkThe latest tag is the released build (cut from main). For the bleeding edge from the integration branch, pull the
dev prerelease tag (published on develop); it is never the default install:
npm i @metatrongg/sdk@devzod is the only runtime dependency. react (for @metatrongg/sdk/react) and your web framework (for the webhook
adapters) are optional peers — install them only if you use those entry points.
Type-safe + validated. Types and runtime zod validators are generated from the Metatron API's OpenAPI document, so
the client always matches the live API. Every response is parsed against its validator before you receive it — the shape
you get is the shape the API promises. The SDK depends on no other package; zod is its only runtime dependency.
Docs
- Guides — https://metatron.gg/docs
- SDK reference — https://metatron.gg/sdk
- Raw HTTP / OpenAPI — https://metatron.gg/reference
- Building with an agent? — https://metatron.gg/llms.txt
Exports
Subpath exports separate the secret-bearing primitives from the browser surface.
| Subpath | Use it from | Holds |
|---|---|---|
@metatrongg/sdk |
any | error model, token store types, logger types, webhook verify (universal) |
@metatrongg/sdk/browser |
browser | PKCE flow, payments user-bearer, reads, TronBrowserClient, cookie store |
@metatrongg/sdk/node |
node | everything above + client_credentials, payouts, events socket, TronNodeClient |
@metatrongg/sdk/react |
react | createTronReact() -- provider + useTronClient / useTronQuery / useTronMutation |
@metatrongg/sdk/contracts |
any | spec-generated zod validators + types + z/ZodError for one-import safeParse |
@metatrongg/sdk/webhook |
any | framework-agnostic HMAC verify |
@metatrongg/sdk/webhook/hono |
server | hono middleware adapter |
@metatrongg/sdk/webhook/express |
server | express middleware adapter |
@metatrongg/sdk/webhook/fastify |
server | fastify pre-handler adapter |
The browser path excludes every client_secret-bearing surface. A bundler that imports the browser entry into a browser
app cannot accidentally pull client_credentials or payouts.
Client-side safety: rate limiting + request de-duplication
The api enforces the real limits; the SDK adds courtesy guards so a well-behaved client paces itself instead of eating 429s, and so accidental fan-out doesn't hammer the network:
- Rate limiting mirrors the server's per-user policy (reads 60 / 60s, mutations 5 / 60s) with separate read/write
token buckets, keyed by HTTP method. On by default; tune with
rateLimit: { read, write }or disable withrateLimit: falseon any client config. - Request de-duplication coalesces concurrent identical GETs onto one round trip (a common React double-render /
fan-out pattern). On by default; disable with
dedupe: false. Mutations are never de-duplicated.
Both live in the transport, so every method -- and the React hooks below -- get them for free.
Quickstart: React
@metatrongg/sdk/react ships a typed factory; react is an optional peer dependency.
import { TronBrowserClient } from "@metatrongg/sdk/browser";
import { createTronReact } from "@metatrongg/sdk/react";
const client = new TronBrowserClient({ issuer, clientId, redirectUri, scopes: ["openid", "profile"] });
export const { TronProvider, useTronClient, useTronQuery, useTronMutation } = createTronReact<TronBrowserClient>();
// Wrap once: <TronProvider client={client}>...</TronProvider>
function Profile({ handle }: { handle: string }) {
const { data, loading, error } = useTronQuery((c) => c.users.get({ handle }), [handle]);
if (loading) return <Spinner />;
if (error) return <ErrorCard error={error} />;
return <ProfileCard profile={data} />;
}Quickstart: browser PKCE flow
The user clicks "sign in"; the SDK builds the authorize URL with a fresh PKCE pair + state. Store the verifier + state in a session-scoped place (cookie, sessionStorage) before redirecting.
import { TronBrowserClient } from "@metatrongg/sdk/browser";
const client = new TronBrowserClient({
issuer: "https://api.metatron.gg",
clientId: "tg_prod_yourappid",
redirectUri: "https://yourgame.example/auth/callback",
scopes: ["openid", "profile", "payments:charge"],
});
// 1) Build the authorize URL + redirect
const { url, state, codeVerifier } = await client.oauth.buildAuthorizeUrl();
sessionStorage.setItem("tron_oauth_state", state);
sessionStorage.setItem("tron_oauth_verifier", codeVerifier);
window.location.assign(url);
// 2) On the redirect-uri callback page
const params = new URLSearchParams(window.location.search);
const code = params.get("code")!;
const returnedState = params.get("state");
const expectedState = sessionStorage.getItem("tron_oauth_state");
const codeVerifier = sessionStorage.getItem("tron_oauth_verifier")!;
if (returnedState !== expectedState) throw new Error("state mismatch");
const tokens = await client.oauth.exchangeCode({ code, codeVerifier });
// tokens.accessToken is now ready for `client.payments.*` and `client.users.*` callsQuickstart: node app-bearer flow
The game backend mints a client_credentials token to drive payouts. The SDK caches the bearer with preemptive refresh.
import { TronNodeClient } from "@metatrongg/sdk/node";
const client = new TronNodeClient({
issuer: process.env.TRON_API_ORIGIN!,
clientId: process.env.OAUTH_CLIENT_ID!,
clientSecret: process.env.OAUTH_CLIENT_SECRET!,
});
// One-line payout. The SDK mints + caches the app bearer; no manual /oauth/token round-trip.
const result = await client.payments.payout({
body: {
chain: "base-mainnet",
token: "0x0000000000000000000000000000000000000000",
amount: "1000000000000000000", // 1 ETH-equivalent in wei
recipientUserId: "u_abc123",
},
});
if (result.kind === "direct") {
console.log("paid out atomically", result.txHash);
} else {
console.log("ledger-credited to recipient vault", result.txHash);
}Quickstart: events socket
Long-lived subscriber to /oauth/payments/events. 1s → 30s exponential backoff with jitter; {kind:"subscribed"} first
frame is a no-op; subsequent frames are OauthPaymentWebhookBody.
import { TronNodeClient } from "@metatrongg/sdk/node";
const client = new TronNodeClient({ ... });
const subscription = client.subscribeOauthPaymentEvents({
onSubscribed: (appId) => console.log("tron events subscribed", appId),
onMessage: async (body) => {
if (body.event === "intent.completed") {
await markIntentPaid(body.intent.intentId, body.intent.txHash);
}
},
onClose: (error) => {
if (error.isPermanent) console.error("tron events: stopping", error.code, error.reason);
},
});
// On shutdown:
process.on("SIGTERM", () => subscription.stop());Quickstart: webhook receiver
HMAC-SHA256 over <timestamp>.<body> with the per-app payment_status_webhook_secret. Hono middleware shown; express +
fastify adapters mirror the shape.
import { Hono } from "hono";
import { createMetatronWebhookMiddleware } from "@metatrongg/sdk/webhook/hono";
const app = new Hono();
app.post("/webhooks/tron", createMetatronWebhookMiddleware({ secret: process.env.TRON_WEBHOOK_SECRET! }), (c) => {
const body = c.get("tronWebhook");
if (body.event === "intent.completed") {
// ... persist body.intent ...
}
return c.json({ ok: true });
});Without a framework, use the universal verify:
import { verifyWebhook, WEBHOOK_SIGNATURE_HEADER, WEBHOOK_TIMESTAMP_HEADER } from "@metatrongg/sdk/webhook";
const rawBody = await req.text();
const result = await verifyWebhook({
secret: process.env.TRON_WEBHOOK_SECRET!,
rawBody,
signatureHeader: req.headers.get(WEBHOOK_SIGNATURE_HEADER),
timestampHeader: req.headers.get(WEBHOOK_TIMESTAMP_HEADER),
});
if (!result.ok) return new Response(JSON.stringify({ reason: result.reason }), { status: 400 });
// result.body is the parsed OauthPaymentWebhookBodyError model
TronError-- base shape; carriesstatus,code,body.TronOauthError-- RFC 6749 §5.2; carrieserror/error_description/error_uri.TronWsCloseError-- WS application close codes (4400 / 4401 / 4403 / 4404 / 4503); theisPermanentgetter returnstruefor the auth-shaped codes so the caller can stop the loop.charge()returns{ status: "monthly_limit_exceeded", ... }as a value (not an exception) so the caller can drive the raise-cap UX without try/catching.
Token storage
The TokenStore interface is get / set / clear. Two helpers ship out of the box:
createInMemoryTokenStore()-- process-local; default for tests + SPAs that ferry tokens via cookies.createCookieTokenStore()-- browser cookie-backed (Secure, SameSite=Lax).
Node deployments wire their own DB-backed adapter -- the SDK never persists anything itself.