npm.io
0.8.0-dev.c1e4667 • Published 40m ago

@metatrongg/sdk

Licence
SEE LICENSE IN LICENSE
Version
0.8.0-dev.c1e4667
Deps
1
Size
2.0 MB
Vulns
0
Weekly
4.2K

@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/sdk

The 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@dev

zod 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

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 with rateLimit: false on 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.*` calls

Quickstart: 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 OauthPaymentWebhookBody

Error model

  • TronError -- base shape; carries status, code, body.
  • TronOauthError -- RFC 6749 §5.2; carries error / error_description / error_uri.
  • TronWsCloseError -- WS application close codes (4400 / 4401 / 4403 / 4404 / 4503); the isPermanent getter returns true for 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.

Keywords