@finrock/mcp 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/LICENSE +7 -0
  2. package/README.md +214 -2
  3. package/dist/api/client.d.ts +50 -0
  4. package/dist/api/client.js +172 -0
  5. package/dist/api/tools.d.ts +17 -0
  6. package/dist/api/tools.js +300 -0
  7. package/dist/docs.d.ts +61 -0
  8. package/dist/docs.js +190 -0
  9. package/dist/guides.d.ts +19 -0
  10. package/dist/guides.js +66 -0
  11. package/dist/http.d.ts +13 -0
  12. package/dist/http.js +94 -0
  13. package/dist/index.d.ts +7 -0
  14. package/dist/index.js +7 -0
  15. package/dist/server.d.ts +17 -0
  16. package/dist/server.js +229 -0
  17. package/dist/setup.d.ts +10 -0
  18. package/dist/setup.js +123 -0
  19. package/dist/stdio.d.ts +2 -0
  20. package/dist/stdio.js +28 -0
  21. package/dist/webhook.d.ts +11 -0
  22. package/dist/webhook.js +39 -0
  23. package/guides/_client/nextjs.md +57 -0
  24. package/guides/_client/node.md +55 -0
  25. package/guides/_client/php.md +67 -0
  26. package/guides/_client/python.md +55 -0
  27. package/guides/accept-payments/nextjs.md +75 -0
  28. package/guides/accept-payments/node.md +49 -0
  29. package/guides/accept-payments/overview.md +30 -0
  30. package/guides/accept-payments/php.md +61 -0
  31. package/guides/accept-payments/python.md +59 -0
  32. package/guides/authentication/overview.md +60 -0
  33. package/guides/deposit-addresses/nextjs.md +50 -0
  34. package/guides/deposit-addresses/node.md +30 -0
  35. package/guides/deposit-addresses/overview.md +36 -0
  36. package/guides/deposit-addresses/php.md +35 -0
  37. package/guides/deposit-addresses/python.md +31 -0
  38. package/guides/payment-widget/html.md +26 -0
  39. package/guides/payment-widget/nextjs.md +41 -0
  40. package/guides/payment-widget/overview.md +32 -0
  41. package/guides/webhooks/nextjs.md +48 -0
  42. package/guides/webhooks/node.md +48 -0
  43. package/guides/webhooks/overview.md +34 -0
  44. package/guides/webhooks/php.md +46 -0
  45. package/guides/webhooks/python.md +55 -0
  46. package/guides/withdrawals/nextjs.md +35 -0
  47. package/guides/withdrawals/node.md +35 -0
  48. package/guides/withdrawals/overview.md +32 -0
  49. package/guides/withdrawals/php.md +37 -0
  50. package/guides/withdrawals/python.md +33 -0
  51. package/package.json +55 -4
@@ -0,0 +1,55 @@
1
+ ### Finrock API client (`finrock.ts`)
2
+
3
+ No SDK needed: this signs each request with Node's built-in `crypto`.
4
+
5
+ ```ts
6
+ // finrock.ts — server-side only. Never import this into browser code.
7
+ import { createHash, createSign, randomUUID } from "node:crypto";
8
+
9
+ const API_BASE = "https://api.finrock.io";
10
+ const API_KEY = process.env.FINROCK_API_KEY!; // API Key UUID from Finrock Dashboard → API Users
11
+ // PEM of the RSA-4096 private key whose public half you uploaded. Env vars often
12
+ // store newlines as "\n", so restore them.
13
+ const PRIVATE_KEY = process.env.FINROCK_PRIVATE_KEY!.replace(/\\n/g, "\n");
14
+
15
+ const b64url = (b: Buffer | string) => Buffer.from(b).toString("base64url");
16
+
17
+ /** A fresh RS256 JWT for one request. Finrock rejects reused nonces and exp > iat + 30s. */
18
+ function signJwt(uri: string, rawBody: string): string {
19
+ const now = Math.floor(Date.now() / 1000);
20
+ const header = { alg: "RS256", typ: "JWT" };
21
+ const payload = {
22
+ uri, // request path, e.g. /dac/v1/invoice
23
+ nonce: randomUUID(),
24
+ iat: now,
25
+ exp: now + 25,
26
+ sub: API_KEY,
27
+ bodyHash: createHash("sha256").update(rawBody).digest("hex"),
28
+ };
29
+ const input = `${b64url(JSON.stringify(header))}.${b64url(JSON.stringify(payload))}`;
30
+ const signature = createSign("RSA-SHA256").update(input).sign(PRIVATE_KEY);
31
+ return `${input}.${b64url(signature)}`;
32
+ }
33
+
34
+ export async function finrock<T = any>(
35
+ method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE",
36
+ path: string,
37
+ body?: unknown,
38
+ ): Promise<T> {
39
+ // Hash exactly the string you send: serialise once and reuse it.
40
+ const rawBody = body === undefined ? "" : JSON.stringify(body);
41
+ const res = await fetch(API_BASE + path, {
42
+ method,
43
+ headers: {
44
+ authorization: `Bearer ${signJwt(path, rawBody)}`,
45
+ "x-api-key": API_KEY,
46
+ accept: "application/json",
47
+ ...(rawBody ? { "content-type": "application/json" } : {}),
48
+ },
49
+ body: rawBody || undefined,
50
+ });
51
+ const text = await res.text();
52
+ if (!res.ok) throw new Error(`Finrock ${method} ${path} -> HTTP ${res.status}: ${text}`);
53
+ return (text ? JSON.parse(text) : null) as T;
54
+ }
55
+ ```
@@ -0,0 +1,67 @@
1
+ ### Finrock API client (`finrock.php`)
2
+
3
+ Uses only the `openssl`, `curl` and `json` extensions.
4
+
5
+ ```php
6
+ <?php
7
+ // finrock.php — server-side only.
8
+ const FINROCK_API_BASE = 'https://api.finrock.io';
9
+
10
+ function finrock_b64url(string $s): string
11
+ {
12
+ return rtrim(strtr(base64_encode($s), '+/', '-_'), '=');
13
+ }
14
+
15
+ /** A fresh RS256 JWT for one request. Finrock rejects reused nonces and exp > iat + 30s. */
16
+ function finrock_sign(string $uri, string $rawBody): string
17
+ {
18
+ $now = time();
19
+ $header = ['alg' => 'RS256', 'typ' => 'JWT'];
20
+ $payload = [
21
+ 'uri' => $uri, // request path, e.g. /dac/v1/invoice
22
+ 'nonce' => bin2hex(random_bytes(16)),
23
+ 'iat' => $now,
24
+ 'exp' => $now + 25,
25
+ 'sub' => getenv('FINROCK_API_KEY'), // API Key UUID from Finrock Dashboard -> API Users
26
+ 'bodyHash' => hash('sha256', $rawBody),
27
+ ];
28
+ $input = finrock_b64url(json_encode($header)) . '.' . finrock_b64url(json_encode($payload, JSON_UNESCAPED_SLASHES));
29
+ // PEM of the RSA-4096 private key whose public half you uploaded.
30
+ $key = openssl_pkey_get_private(str_replace('\n', "\n", getenv('FINROCK_PRIVATE_KEY')));
31
+ if ($key === false || !openssl_sign($input, $signature, $key, OPENSSL_ALGO_SHA256)) {
32
+ throw new RuntimeException('Could not sign the Finrock JWT: check FINROCK_PRIVATE_KEY');
33
+ }
34
+ return $input . '.' . finrock_b64url($signature);
35
+ }
36
+
37
+ function finrock(string $method, string $path, ?array $body = null)
38
+ {
39
+ // Hash exactly the string you send: serialise once and reuse it.
40
+ $rawBody = $body === null ? '' : json_encode($body, JSON_UNESCAPED_SLASHES);
41
+ $headers = [
42
+ 'Authorization: Bearer ' . finrock_sign($path, $rawBody),
43
+ 'x-api-key: ' . getenv('FINROCK_API_KEY'),
44
+ 'Accept: application/json',
45
+ ];
46
+ if ($rawBody !== '') {
47
+ $headers[] = 'Content-Type: application/json';
48
+ }
49
+ $ch = curl_init(FINROCK_API_BASE . $path);
50
+ curl_setopt_array($ch, [
51
+ CURLOPT_CUSTOMREQUEST => $method,
52
+ CURLOPT_HTTPHEADER => $headers,
53
+ CURLOPT_RETURNTRANSFER => true,
54
+ CURLOPT_TIMEOUT => 30,
55
+ ]);
56
+ if ($rawBody !== '') {
57
+ curl_setopt($ch, CURLOPT_POSTFIELDS, $rawBody);
58
+ }
59
+ $response = curl_exec($ch);
60
+ $status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
61
+ curl_close($ch);
62
+ if ($response === false || $status >= 400) {
63
+ throw new RuntimeException("Finrock $method $path -> HTTP $status: $response");
64
+ }
65
+ return $response === '' ? null : json_decode($response, true);
66
+ }
67
+ ```
@@ -0,0 +1,55 @@
1
+ ### Finrock API client (`finrock.py`)
2
+
3
+ ```bash
4
+ pip install "pyjwt[crypto]" requests
5
+ ```
6
+
7
+ ```python
8
+ # finrock.py — server-side only.
9
+ import hashlib
10
+ import json
11
+ import os
12
+ import time
13
+ import uuid
14
+
15
+ import jwt # PyJWT
16
+ import requests
17
+
18
+ API_BASE = "https://api.finrock.io"
19
+ API_KEY = os.environ["FINROCK_API_KEY"] # API Key UUID from Finrock Dashboard -> API Users
20
+ # PEM of the RSA-4096 private key whose public half you uploaded.
21
+ PRIVATE_KEY = os.environ["FINROCK_PRIVATE_KEY"].replace("\\n", "\n")
22
+
23
+
24
+ def _sign(uri: str, raw_body: str) -> str:
25
+ """A fresh RS256 JWT for one request. Finrock rejects reused nonces and exp > iat + 30s."""
26
+ now = int(time.time())
27
+ payload = {
28
+ "uri": uri, # request path, e.g. /dac/v1/invoice
29
+ "nonce": str(uuid.uuid4()),
30
+ "iat": now,
31
+ "exp": now + 25,
32
+ "sub": API_KEY,
33
+ "bodyHash": hashlib.sha256(raw_body.encode("utf-8")).hexdigest(),
34
+ }
35
+ return jwt.encode(payload, PRIVATE_KEY, algorithm="RS256")
36
+
37
+
38
+ def finrock(method: str, path: str, body=None):
39
+ # Hash exactly the bytes you send: serialise once and reuse them.
40
+ raw_body = "" if body is None else json.dumps(body, separators=(",", ":"))
41
+ headers = {
42
+ "Authorization": f"Bearer {_sign(path, raw_body)}",
43
+ "x-api-key": API_KEY,
44
+ "Accept": "application/json",
45
+ }
46
+ if raw_body:
47
+ headers["Content-Type"] = "application/json"
48
+ res = requests.request(
49
+ method, API_BASE + path, data=raw_body.encode("utf-8") if raw_body else None,
50
+ headers=headers, timeout=30,
51
+ )
52
+ if not res.ok:
53
+ raise RuntimeError(f"Finrock {method} {path} -> HTTP {res.status_code}: {res.text}")
54
+ return res.json() if res.text else None
55
+ ```
@@ -0,0 +1,75 @@
1
+ ### Server Action to start checkout (`app/checkout/actions.ts`)
2
+
3
+ ```ts
4
+ "use server";
5
+ import { redirect } from "next/navigation";
6
+ import { finrock } from "@/lib/finrock";
7
+ import { db } from "@/lib/db"; // your data layer
8
+
9
+ const BASE_URL = process.env.PUBLIC_BASE_URL!; // e.g. https://shop.example.com
10
+
11
+ export async function payWithCrypto(orderId: string) {
12
+ const order = await db.orders.get(orderId);
13
+ const invoice = await finrock("POST", "/dac/v1/invoice", {
14
+ fiat: { asset_id: order.currency, amount: order.total }, // e.g. "USD", 49.99
15
+ crypto: { asset_id: "USDT_TRX" }, // the coin to collect; Finrock converts the fiat amount
16
+ memo: `order-${order.id}`,
17
+ payer: { email: order.email },
18
+ ipn_url: `${BASE_URL}/api/finrock/ipn?order=${order.id}`,
19
+ success_url: `${BASE_URL}/orders/${order.id}/thanks`,
20
+ failed_url: `${BASE_URL}/orders/${order.id}/payment-failed`,
21
+ });
22
+ await db.orders.update(order.id, { finrockInvoiceId: invoice.id, paymentStatus: invoice.status });
23
+ redirect(invoice.payment_url);
24
+ }
25
+ ```
26
+
27
+ ### Shared status check (`lib/sync-invoice.ts`)
28
+
29
+ ```ts
30
+ import "server-only";
31
+ import { finrock } from "@/lib/finrock";
32
+ import { db } from "@/lib/db";
33
+
34
+ // Re-check the invoice with Finrock instead of trusting the notification or redirect.
35
+ export async function syncInvoice(orderId: string) {
36
+ const order = await db.orders.get(orderId);
37
+ if (!order?.finrockInvoiceId) return order;
38
+ const [invoice] = await finrock("GET", `/dac/v1/invoice?id=${encodeURIComponent(order.finrockInvoiceId)}`);
39
+ if (invoice?.status === "Paid" && order.paymentStatus !== "Paid") {
40
+ await db.orders.update(orderId, { paymentStatus: "Paid" });
41
+ await fulfil(orderId); // must be safe to call twice
42
+ } else if (invoice && invoice.status !== order.paymentStatus) {
43
+ await db.orders.update(orderId, { paymentStatus: invoice.status });
44
+ }
45
+ return db.orders.get(orderId);
46
+ }
47
+ ```
48
+
49
+ ### Notification endpoint (`app/api/finrock/ipn/route.ts`)
50
+
51
+ ```ts
52
+ import { syncInvoice } from "@/lib/sync-invoice";
53
+
54
+ export const runtime = "nodejs";
55
+
56
+ export async function POST(req: Request) {
57
+ const orderId = new URL(req.url).searchParams.get("order");
58
+ if (orderId) await syncInvoice(orderId);
59
+ return new Response("ok", { status: 200 });
60
+ }
61
+ ```
62
+
63
+ ### Thank-you page (`app/orders/[id]/thanks/page.tsx`)
64
+
65
+ ```tsx
66
+ import { syncInvoice } from "@/lib/sync-invoice";
67
+
68
+ export default async function Thanks({ params }: { params: Promise<{ id: string }> }) {
69
+ const { id } = await params;
70
+ const order = await syncInvoice(id);
71
+ return order.paymentStatus === "Paid"
72
+ ? <p>Payment received. Thank you!</p>
73
+ : <p>We are waiting for your payment to confirm on-chain ({order.paymentStatus}).</p>;
74
+ }
75
+ ```
@@ -0,0 +1,49 @@
1
+ ### Express routes (`server.ts`)
2
+
3
+ ```ts
4
+ import express from "express";
5
+ import { finrock } from "./finrock";
6
+
7
+ const app = express();
8
+ app.use(express.json());
9
+ const BASE_URL = process.env.PUBLIC_BASE_URL!; // e.g. https://shop.example.com
10
+
11
+ app.post("/checkout/:orderId/crypto", async (req, res) => {
12
+ const order = await db.orders.get(req.params.orderId); // your data layer
13
+ const invoice = await finrock("POST", "/dac/v1/invoice", {
14
+ fiat: { asset_id: order.currency, amount: order.total }, // e.g. "USD", 49.99
15
+ crypto: { asset_id: "USDT_TRX" }, // the coin to collect; Finrock converts the fiat amount
16
+ memo: `order-${order.id}`,
17
+ payer: { email: order.email },
18
+ ipn_url: `${BASE_URL}/finrock/ipn?order=${order.id}`,
19
+ success_url: `${BASE_URL}/orders/${order.id}/thanks`,
20
+ failed_url: `${BASE_URL}/orders/${order.id}/payment-failed`,
21
+ });
22
+ await db.orders.update(order.id, { finrockInvoiceId: invoice.id, paymentStatus: invoice.status });
23
+ res.redirect(303, invoice.payment_url);
24
+ });
25
+
26
+ // Re-check the invoice with Finrock instead of trusting the notification body.
27
+ async function syncInvoice(orderId: string) {
28
+ const order = await db.orders.get(orderId);
29
+ if (!order?.finrockInvoiceId) return;
30
+ const [invoice] = await finrock("GET", `/dac/v1/invoice?id=${encodeURIComponent(order.finrockInvoiceId)}`);
31
+ if (!invoice) return;
32
+ if (invoice.status === "Paid" && order.paymentStatus !== "Paid") {
33
+ await db.orders.update(orderId, { paymentStatus: "Paid" });
34
+ await fulfil(orderId); // must be safe to call twice
35
+ } else if (invoice.status !== order.paymentStatus) {
36
+ await db.orders.update(orderId, { paymentStatus: invoice.status });
37
+ }
38
+ }
39
+
40
+ app.post("/finrock/ipn", async (req, res) => {
41
+ await syncInvoice(String(req.query.order));
42
+ res.sendStatus(200);
43
+ });
44
+
45
+ app.get("/orders/:orderId/thanks", async (req, res) => {
46
+ await syncInvoice(req.params.orderId);
47
+ res.render("thanks", { order: await db.orders.get(req.params.orderId) });
48
+ });
49
+ ```
@@ -0,0 +1,30 @@
1
+ ---
2
+ title: Accept a crypto payment with a hosted invoice
3
+ summary: Create an invoice on your server, send the customer to Finrock's hosted payment page, and mark the order paid after re-checking the invoice.
4
+ docs: api-reference/invoicing/create-invoice, api-reference/invoicing/list-invoices, reference/supported-assets
5
+ client: true
6
+ ---
7
+
8
+ This is the simplest way to take crypto payments for an order, subscription or invoice.
9
+
10
+ ### Flow
11
+
12
+ 1. **Customer checks out.** Your server calls `POST /dac/v1/invoice` with the price and a
13
+ unique `memo` (your order ID works well).
14
+ - `crypto.asset_id` is always required: the coin to collect, e.g. `crypto: { asset_id: "USDT_TRX" }`
15
+ (asset IDs come from Supported Assets). Without it the API returns `ASSET_REQUIRED`.
16
+ - Price in fiat with `fiat: { asset_id: "USD", amount }` and Finrock converts it to the coin, or
17
+ - give `crypto.amount` to price in the coin directly.
18
+ - Set `success_url`, `failed_url`, and `ipn_url` (payment notifications).
19
+ 2. **Save** the returned invoice `id` against your order, then **redirect** the customer to
20
+ the returned `payment_url`.
21
+ 3. **Confirm payment on your server.** When the `ipn_url` is called, or the customer lands on
22
+ `success_url`, fetch the invoice with `GET /dac/v1/invoice?id=<invoice id>` and fulfil only
23
+ when its `status` is `Paid`. Treat the redirect and notification as hints, never as proof.
24
+ 4. **Make fulfilment idempotent.** Notifications and redirects can arrive more than once.
25
+
26
+ Invoice statuses: `Pending`, `Processing`, `Paid`, `Expired`, `Failed`, `Cancelled`, `Refunded`.
27
+ Invoices expire at `expiry_ts`; late payments may need manual reconciliation.
28
+
29
+ Put your own order ID in the `ipn_url` query string (as below) so the notification handler can
30
+ find the order without depending on the notification body, whose format is not documented yet.
@@ -0,0 +1,61 @@
1
+ ### Start checkout (`checkout_crypto.php`)
2
+
3
+ ```php
4
+ <?php
5
+ require __DIR__ . '/finrock.php';
6
+ require __DIR__ . '/db.php'; // your data layer
7
+
8
+ $baseUrl = getenv('PUBLIC_BASE_URL'); // e.g. https://shop.example.com
9
+ $order = db_get_order($_POST['order_id']);
10
+
11
+ $invoice = finrock('POST', '/dac/v1/invoice', [
12
+ 'fiat' => ['asset_id' => $order['currency'], 'amount' => (float) $order['total']], // e.g. "USD", 49.99
13
+ 'crypto' => ['asset_id' => 'USDT_TRX'], // the coin to collect; Finrock converts the fiat amount
14
+ 'memo' => 'order-' . $order['id'],
15
+ 'payer' => ['email' => $order['email']],
16
+ 'ipn_url' => "$baseUrl/finrock_ipn.php?order=" . urlencode($order['id']),
17
+ 'success_url' => "$baseUrl/thanks.php?order=" . urlencode($order['id']),
18
+ 'failed_url' => "$baseUrl/payment_failed.php?order=" . urlencode($order['id']),
19
+ ]);
20
+
21
+ db_update_order($order['id'], ['finrock_invoice_id' => $invoice['id'], 'payment_status' => $invoice['status']]);
22
+ header('Location: ' . $invoice['payment_url'], true, 303);
23
+ ```
24
+
25
+ ### Shared status check (`sync_invoice.php`)
26
+
27
+ ```php
28
+ <?php
29
+ require_once __DIR__ . '/finrock.php';
30
+ require_once __DIR__ . '/db.php';
31
+
32
+ // Re-check the invoice with Finrock instead of trusting the notification body.
33
+ function sync_invoice(string $orderId): ?array
34
+ {
35
+ $order = db_get_order($orderId);
36
+ if (!$order || empty($order['finrock_invoice_id'])) {
37
+ return $order;
38
+ }
39
+ $invoices = finrock('GET', '/dac/v1/invoice?id=' . rawurlencode($order['finrock_invoice_id'])) ?? [];
40
+ if ($invoices) {
41
+ $status = $invoices[0]['status'];
42
+ if ($status === 'Paid' && $order['payment_status'] !== 'Paid') {
43
+ db_update_order($orderId, ['payment_status' => 'Paid']);
44
+ fulfil($orderId); // must be safe to call twice
45
+ } elseif ($status !== $order['payment_status']) {
46
+ db_update_order($orderId, ['payment_status' => $status]);
47
+ }
48
+ }
49
+ return db_get_order($orderId);
50
+ }
51
+ ```
52
+
53
+ ### Notification endpoint (`finrock_ipn.php`)
54
+
55
+ ```php
56
+ <?php
57
+ require __DIR__ . '/sync_invoice.php';
58
+ sync_invoice($_GET['order'] ?? '');
59
+ http_response_code(200);
60
+ echo 'ok';
61
+ ```
@@ -0,0 +1,59 @@
1
+ ### FastAPI routes (`main.py`)
2
+
3
+ ```python
4
+ import os
5
+ from urllib.parse import quote
6
+
7
+ from fastapi import FastAPI
8
+ from fastapi.responses import RedirectResponse
9
+
10
+ from finrock import finrock
11
+ from mydb import db, fulfil # your data layer
12
+
13
+ app = FastAPI()
14
+ BASE_URL = os.environ["PUBLIC_BASE_URL"] # e.g. https://shop.example.com
15
+
16
+
17
+ @app.post("/checkout/{order_id}/crypto")
18
+ def start_crypto_checkout(order_id: str):
19
+ order = db.get_order(order_id)
20
+ invoice = finrock("POST", "/dac/v1/invoice", {
21
+ "fiat": {"asset_id": order.currency, "amount": float(order.total)}, # e.g. "USD", 49.99
22
+ "crypto": {"asset_id": "USDT_TRX"}, # the coin to collect; Finrock converts the fiat amount
23
+ "memo": f"order-{order.id}",
24
+ "payer": {"email": order.email},
25
+ "ipn_url": f"{BASE_URL}/finrock/ipn?order={order.id}",
26
+ "success_url": f"{BASE_URL}/orders/{order.id}/thanks",
27
+ "failed_url": f"{BASE_URL}/orders/{order.id}/payment-failed",
28
+ })
29
+ db.update_order(order.id, finrock_invoice_id=invoice["id"], payment_status=invoice["status"])
30
+ return RedirectResponse(invoice["payment_url"], status_code=303)
31
+
32
+
33
+ def sync_invoice(order_id: str):
34
+ """Re-check the invoice with Finrock instead of trusting the notification body."""
35
+ order = db.get_order(order_id)
36
+ if not order or not order.finrock_invoice_id:
37
+ return order
38
+ invoices = finrock("GET", f"/dac/v1/invoice?id={quote(order.finrock_invoice_id)}") or []
39
+ if invoices:
40
+ status = invoices[0]["status"]
41
+ if status == "Paid" and order.payment_status != "Paid":
42
+ db.update_order(order_id, payment_status="Paid")
43
+ fulfil(order_id) # must be safe to call twice
44
+ elif status != order.payment_status:
45
+ db.update_order(order_id, payment_status=status)
46
+ return db.get_order(order_id)
47
+
48
+
49
+ @app.post("/finrock/ipn")
50
+ def finrock_ipn(order: str):
51
+ sync_invoice(order)
52
+ return {"ok": True}
53
+
54
+
55
+ @app.get("/orders/{order_id}/thanks")
56
+ def thanks(order_id: str):
57
+ order = sync_invoice(order_id)
58
+ return {"status": order.payment_status}
59
+ ```
@@ -0,0 +1,60 @@
1
+ ---
2
+ title: Authenticate with the Finrock API
3
+ summary: Create an API user, generate an RSA key pair, and sign every request with a short-lived JWT.
4
+ docs: quickstart, api-reference/authentication, guides/whitelist-ips, reference/http-status-codes
5
+ client: true
6
+ ---
7
+
8
+ Every Finrock API call (except the public utilities such as Supported Assets and Asset Price)
9
+ needs two headers:
10
+
11
+ - `x-api-key`: the API Key UUID of an API user.
12
+ - `Authorization: Bearer <JWT>`: a JWT signed with **RS256** using your own RSA-4096 private key.
13
+
14
+ ### One-time setup
15
+
16
+ 1. Generate a key pair. The private key never leaves your servers.
17
+ ```bash
18
+ openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:4096 -out finrock_private.pem
19
+ openssl pkey -in finrock_private.pem -pubout -out finrock_public.pem
20
+ ```
21
+ 2. In the Finrock Dashboard, open **API Users**, create a user, upload `finrock_public.pem`
22
+ and copy the API Key UUID (it is shown only once).
23
+ 3. Store both as server-side secrets:
24
+ ```bash
25
+ FINROCK_API_KEY=4466c45a-... # the API Key UUID
26
+ FINROCK_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----"
27
+ ```
28
+ 4. Optional but recommended: restrict the key to your server IPs (see *Whitelist IPs*).
29
+
30
+ ### Rules for the JWT
31
+
32
+ | Claim | Value |
33
+ |---|---|
34
+ | `uri` | The request path, e.g. `/dac/v1/invoice` |
35
+ | `nonce` | A new random value (UUID) for every request; reuse is rejected |
36
+ | `iat` | Now, in seconds since the epoch |
37
+ | `exp` | At most `iat + 30`; use `iat + 25` to allow for clock skew |
38
+ | `sub` | Your API Key UUID |
39
+ | `bodyHash` | Hex SHA-256 of the exact raw body string you send (`""` when there is no body) |
40
+
41
+ Build a new JWT for every request. Serialise the body once, hash that exact string, and send
42
+ that same string; pretty-printing or re-serialising after hashing causes a `bodyHash mismatch` 401.
43
+
44
+ ### Never do this
45
+
46
+ - Never call the Finrock API from browser or mobile code; the private key and API key would leak.
47
+ Browsers use the Payment Widget or a hosted invoice `payment_url` instead.
48
+ - Never commit `finrock_private.pem` or put it in client-side environment variables
49
+ (`NEXT_PUBLIC_*`, `VITE_*`, and so on).
50
+
51
+ ### Troubleshooting 401s
52
+
53
+ - *Token expired or window too large*: server clock drift, or `exp` more than 30 s after `iat`.
54
+ - *Invalid signature*: wrong algorithm (must be RS256) or the uploaded public key doesn't match.
55
+ - *bodyHash mismatch*: the body sent differs from the string that was hashed.
56
+ - *Nonce already used*: a JWT was reused, e.g. on a retry. Sign again for each attempt.
57
+
58
+ > Open question to confirm with Finrock: for GET requests with a query string, this client signs
59
+ > the path including the query (e.g. `/dac/v1/invoice?id=...`). If you get a signature 401 on
60
+ > such a call, try signing the path without the query string.
@@ -0,0 +1,50 @@
1
+ ### Address provisioning (`deposit-addresses.ts`)
2
+
3
+ ```ts
4
+ import { finrock } from "@/lib/finrock";
5
+
6
+ // One-time setup per asset (run from a script). Errors if the wallet already exists.
7
+ export async function createWallet(asset: string) {
8
+ return finrock("POST", "/dac/v1/wallet", { asset });
9
+ }
10
+
11
+ // Get or create the deposit address for a customer and asset.
12
+ export async function depositAddressFor(customerId: string, asset: string) {
13
+ const existing = await db.depositAddresses.find({ customerId, asset }); // your data layer
14
+ if (existing) return existing;
15
+
16
+ const created = await finrock("POST", "/dac/v1/generate-address", {
17
+ asset, // e.g. "USDT_TRX"
18
+ label: `customer-${customerId}`,
19
+ omnibus: false,
20
+ auto_refill: true,
21
+ });
22
+ return db.depositAddresses.insert({
23
+ customerId,
24
+ asset,
25
+ address: created.address,
26
+ addressTag: created.address_tag, // needed for XRP / XLM
27
+ label: created.label,
28
+ });
29
+ }
30
+ ```
31
+
32
+ ### Show it to the customer (`app/wallet/[asset]/page.tsx`)
33
+
34
+ ```tsx
35
+ import { depositAddressFor } from "@/lib/deposit-addresses";
36
+ import { getCurrentUser } from "@/lib/auth"; // your auth
37
+
38
+ export default async function DepositPage({ params }: { params: Promise<{ asset: string }> }) {
39
+ const { asset } = await params;
40
+ const user = await getCurrentUser();
41
+ const addr = await depositAddressFor(user.id, asset);
42
+ return (
43
+ <div>
44
+ <p>Send only {asset} to this address:</p>
45
+ <code>{addr.address}</code>
46
+ {addr.addressTag && <p>Destination tag / memo: <code>{addr.addressTag}</code></p>}
47
+ </div>
48
+ );
49
+ }
50
+ ```
@@ -0,0 +1,30 @@
1
+ ### Address provisioning (`deposit-addresses.ts`)
2
+
3
+ ```ts
4
+ import { finrock } from "./finrock";
5
+
6
+ // One-time setup per asset (run from a script). Errors if the wallet already exists.
7
+ export async function createWallet(asset: string) {
8
+ return finrock("POST", "/dac/v1/wallet", { asset });
9
+ }
10
+
11
+ // Get or create the deposit address for a customer and asset.
12
+ export async function depositAddressFor(customerId: string, asset: string) {
13
+ const existing = await db.depositAddresses.find({ customerId, asset }); // your data layer
14
+ if (existing) return existing;
15
+
16
+ const created = await finrock("POST", "/dac/v1/generate-address", {
17
+ asset, // e.g. "USDT_TRX"
18
+ label: `customer-${customerId}`,
19
+ omnibus: false,
20
+ auto_refill: true,
21
+ });
22
+ return db.depositAddresses.insert({
23
+ customerId,
24
+ asset,
25
+ address: created.address,
26
+ addressTag: created.address_tag, // needed for XRP / XLM
27
+ label: created.label,
28
+ });
29
+ }
30
+ ```
@@ -0,0 +1,36 @@
1
+ ---
2
+ title: Give each customer a crypto deposit address
3
+ summary: Create one wallet per asset, generate a labelled deposit address per customer, and credit them from Deposit webhooks.
4
+ docs: guides/create-wallet-address, api-reference/wallets/create-wallet, api-reference/wallets/create-address, concepts/omnibus-wallets, concepts/gas-tanks, reference/supported-assets
5
+ client: true
6
+ ---
7
+
8
+ Use this for top-ups, exchange-style balances, or any app where customers send crypto to an
9
+ address that belongs to them.
10
+
11
+ ### Flow
12
+
13
+ 1. **One-time: create a wallet per asset** with `POST /dac/v1/wallet` `{ "asset": "USDT_TRX" }`.
14
+ A sub-account can hold only one wallet per asset, so creating it twice errors. Do this
15
+ once from a setup script or the dashboard, not on every request.
16
+ 2. **Per customer: generate an address** with `POST /dac/v1/generate-address`:
17
+ - `asset`: the asset name, e.g. `USDT_TRX`, `ETH`, `BTC`.
18
+ - `label`: your customer ID (e.g. `customer-42`) so deposits can be reconciled.
19
+ - `omnibus`: `false` (default) keeps an isolated address; `true` adds it to the shared pool.
20
+ - `auto_refill`: `true` tops up gas from your Gas Tank when needed (always on for omnibus).
21
+ 3. **Store** the returned `address` (and `address_tag`, which XRP/XLM need as a destination tag)
22
+ with the customer and show it to them, ideally with a QR code.
23
+ 4. **Credit deposits from webhooks** (see the `webhooks` guide): on a `Deposit` event with
24
+ status `Success`, find the customer by the deposit address or label, then credit `amount`
25
+ once per event `id`.
26
+
27
+ ### Choosing omnibus vs isolated
28
+
29
+ - **Isolated** keeps each customer's funds on their own address: clean per-customer audit trail,
30
+ but each address needs gas to move funds on EVM/Tron chains (Gas Tanks handle this).
31
+ - **Omnibus** pools deposits for simpler treasury management and is typical for high volume.
32
+
33
+ Read *Omnibus vs Isolated Addresses* and *Gas Tanks* before choosing for production.
34
+
35
+ > Confirm with a test deposit which webhook fields carry your address and label
36
+ > (`dest_id` / `dest_alias` in the samples) before relying on them for crediting.
@@ -0,0 +1,35 @@
1
+ ### Address provisioning (`deposit_addresses.php`)
2
+
3
+ ```php
4
+ <?php
5
+ require_once __DIR__ . '/finrock.php';
6
+ require_once __DIR__ . '/db.php'; // your data layer
7
+
8
+ // One-time setup per asset (run from a script). Errors if the wallet already exists.
9
+ function create_wallet(string $asset): array
10
+ {
11
+ return finrock('POST', '/dac/v1/wallet', ['asset' => $asset]);
12
+ }
13
+
14
+ // Get or create the deposit address for a customer and asset.
15
+ function deposit_address_for(string $customerId, string $asset): array
16
+ {
17
+ $existing = db_find_deposit_address($customerId, $asset);
18
+ if ($existing) {
19
+ return $existing;
20
+ }
21
+ $created = finrock('POST', '/dac/v1/generate-address', [
22
+ 'asset' => $asset, // e.g. "USDT_TRX"
23
+ 'label' => "customer-$customerId",
24
+ 'omnibus' => false,
25
+ 'auto_refill' => true,
26
+ ]);
27
+ return db_insert_deposit_address([
28
+ 'customer_id' => $customerId,
29
+ 'asset' => $asset,
30
+ 'address' => $created['address'],
31
+ 'address_tag' => $created['address_tag'] ?? null, // needed for XRP / XLM
32
+ 'label' => $created['label'],
33
+ ]);
34
+ }
35
+ ```