@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,31 @@
1
+ ### Address provisioning (`deposit_addresses.py`)
2
+
3
+ ```python
4
+ from finrock import finrock
5
+ from mydb import db # your data layer
6
+
7
+
8
+ def create_wallet(asset: str):
9
+ """One-time setup per asset (run from a script). Errors if the wallet already exists."""
10
+ return finrock("POST", "/dac/v1/wallet", {"asset": asset})
11
+
12
+
13
+ def deposit_address_for(customer_id: str, asset: str):
14
+ """Get or create the deposit address for a customer and asset."""
15
+ existing = db.find_deposit_address(customer_id, asset)
16
+ if existing:
17
+ return existing
18
+ created = finrock("POST", "/dac/v1/generate-address", {
19
+ "asset": asset, # e.g. "USDT_TRX"
20
+ "label": f"customer-{customer_id}",
21
+ "omnibus": False,
22
+ "auto_refill": True,
23
+ })
24
+ return db.insert_deposit_address(
25
+ customer_id=customer_id,
26
+ asset=asset,
27
+ address=created["address"],
28
+ address_tag=created.get("address_tag"), # needed for XRP / XLM
29
+ label=created["label"],
30
+ )
31
+ ```
@@ -0,0 +1,26 @@
1
+ ### Plain HTML (works with any backend)
2
+
3
+ ```html
4
+ <div id="finrock-widget"></div>
5
+ <script src="https://finrock.io/widget/widget.js"></script>
6
+ <script>
7
+ loadWidget({
8
+ widget_id: "YOUR_WIDGET_ID",
9
+ fiat_amount: 49.99,
10
+ crypto_asset: "USDT_TRX",
11
+ payer_email: "jane@example.com",
12
+ desc: "order-1042",
13
+ }).then(
14
+ (result) => {
15
+ // UX only: your server confirms the payment from the webhook.
16
+ window.location.href = "/orders/1042/thanks";
17
+ },
18
+ (err) => {
19
+ console.error("Payment failed or cancelled", err);
20
+ },
21
+ );
22
+ </script>
23
+ ```
24
+
25
+ Render the order values (`fiat_amount`, `desc`, `payer_email`) from your server-side template,
26
+ escaped for JavaScript.
@@ -0,0 +1,41 @@
1
+ ### Client component (`components/finrock-widget.tsx`)
2
+
3
+ ```tsx
4
+ "use client";
5
+ import Script from "next/script";
6
+ import { useRouter } from "next/navigation";
7
+ import { useEffect, useState } from "react";
8
+
9
+ declare global {
10
+ interface Window {
11
+ loadWidget?: (opts: Record<string, unknown>) => Promise<unknown>;
12
+ }
13
+ }
14
+
15
+ export function FinrockWidget(props: { orderId: string; amount: number; asset?: string; email?: string }) {
16
+ const router = useRouter();
17
+ const [ready, setReady] = useState(false);
18
+
19
+ useEffect(() => {
20
+ if (!ready || !window.loadWidget) return;
21
+ window
22
+ .loadWidget({
23
+ widget_id: process.env.NEXT_PUBLIC_FINROCK_WIDGET_ID, // the widget ID is safe to expose
24
+ fiat_amount: props.amount,
25
+ crypto_asset: props.asset,
26
+ payer_email: props.email,
27
+ desc: `order-${props.orderId}`,
28
+ })
29
+ // UX only: your server confirms the payment from the webhook.
30
+ .then(() => router.push(`/orders/${props.orderId}/thanks`))
31
+ .catch((err) => console.error("Payment failed or cancelled", err));
32
+ }, [ready, props.amount, props.asset, props.email, props.orderId, router]);
33
+
34
+ return (
35
+ <>
36
+ <Script src="https://finrock.io/widget/widget.js" strategy="afterInteractive" onReady={() => setReady(true)} />
37
+ <div id="finrock-widget" />
38
+ </>
39
+ );
40
+ }
41
+ ```
@@ -0,0 +1,32 @@
1
+ ---
2
+ title: Embed the Finrock payment widget
3
+ summary: Add a drop-in crypto checkout form to any web page with one script tag and a widget ID; no API keys in the browser.
4
+ docs: guides/payment-widget, reference/supported-assets
5
+ client: false
6
+ ---
7
+
8
+ The widget is the fastest way to accept crypto on a website. It needs only a **widget ID**
9
+ from the Finrock dashboard (ask your account manager if you don't see one); no API key,
10
+ private key or server code is required to show it.
11
+
12
+ ### Options for `loadWidget()`
13
+
14
+ | Option | Type | Purpose |
15
+ |---|---|---|
16
+ | `widget_id` | string | **Required.** Your workspace widget ID |
17
+ | `fiat_amount` | number | Pre-fill the amount in local currency, e.g. `50.00` |
18
+ | `crypto_amount` | number | Pre-fill the crypto amount, e.g. `0.001` |
19
+ | `crypto_asset` | string | Pre-select the coin, e.g. `BTC`, `ETH`, `USDT_TRX` |
20
+ | `payer_name` / `payer_email` / `payer_mobile` | string / string / number | Pre-fill payer details |
21
+ | `desc` | string | Note to you, e.g. your order or invoice number |
22
+
23
+ `loadWidget()` returns a Promise: it resolves with the payment result and rejects on failure
24
+ or cancellation.
25
+
26
+ ### Important
27
+
28
+ - Treat the browser callback as a UX signal only. Confirm the payment on your server from a
29
+ verified webhook (see the `webhooks` guide) before fulfilling, and put your order ID in `desc`
30
+ so you can match it.
31
+ - If you need per-order amounts that customers can't edit, or a redirect-based flow, use the
32
+ `accept-payments` (hosted invoice) guide instead.
@@ -0,0 +1,48 @@
1
+ ### Route Handler (`app/api/webhooks/finrock/route.ts`)
2
+
3
+ ```ts
4
+ import { createPublicKey, verify, constants } from "node:crypto";
5
+ import { db } from "@/lib/db"; // your data layer
6
+
7
+ export const runtime = "nodejs";
8
+ export const dynamic = "force-dynamic";
9
+
10
+ // Finrock's webhook signing key (https://apidocs.finrock.io/guides/webhooks)
11
+ const FINROCK_WEBHOOK_KEY = createPublicKey(`-----BEGIN PUBLIC KEY-----
12
+ MIIBnzANBgkqhkiG9w0BAQEFAAOCAYwAMIIBhwKCAX4Aq1O7ztZxzkgDCFlUnHvs
13
+ EBffqmlng9uEmNMCGbCpxB7991WiR4TRRVaTUrLxIzPWl79xT6R2Xzba1GqN2bin
14
+ sPBU/cmHEF5v1bDopZ7oHnU43tnlpH1kSOjtvTBORf6v6+9xh87OYtg4wXuwVzCq
15
+ ykXDRPIIzgPCSpA7l7DtPqhOpm0OVpUnVJQ5DgnsMfJMPkrrPTjRkpFZ1RbtqoLW
16
+ UI+bMWZAAKLEsh4Q+SRO7xiHj26gToD+/ZrEAbC/SOcy2ChDfpQ39XN/bYTu52j8
17
+ ndAS/R4qnffLGpmTcxr19UC+BPs5Wb5kx2ZJsGPQPpoPHaTBC1n3YwQlksgeHEvX
18
+ ZBsDoAvIXpGfBjUPzd8bvz5vyxbLYA4lSqp/Fw48NYgRRgfOWjmtqVI07jnXKxU2
19
+ kcQc7QkoFVDYw45DS14tkrsCOJjMF0qux2dL9Gr1z/Xdaod+SyeRqsTDYhbzaD+v
20
+ UkO6chtsIHmQwAF8jYajAR7uKROX+6f8ZIwYDco3AgMBAAE=
21
+ -----END PUBLIC KEY-----`);
22
+
23
+ export async function POST(req: Request) {
24
+ // Read raw bytes; don't call req.json() before verifying.
25
+ const raw = Buffer.from(await req.arrayBuffer());
26
+ const signature = req.headers.get("x-signature") ?? "";
27
+ const ok = verify(
28
+ "sha512",
29
+ raw,
30
+ { key: FINROCK_WEBHOOK_KEY, padding: constants.RSA_PKCS1_PADDING },
31
+ Buffer.from(signature, "base64"),
32
+ );
33
+ if (!ok) return new Response("invalid signature", { status: 401 });
34
+
35
+ const event = JSON.parse(raw.toString("utf8"));
36
+
37
+ // Deduplicate: retries resend the same event, and each status change is its own event.
38
+ const firstTime = await db.webhookEvents.insertIfAbsent(`${event.id}:${event.status}`);
39
+ if (!firstTime) return new Response("ok");
40
+
41
+ if (event.type === "Deposit" && event.status === "Success") {
42
+ await creditDeposit(event); // your logic, in a transaction
43
+ } else if (event.type === "Withdraw") {
44
+ await db.withdrawals.updateBySequenceId(event.sequence_id, { status: event.status, hash: event.hash });
45
+ }
46
+ return new Response("ok");
47
+ }
48
+ ```
@@ -0,0 +1,48 @@
1
+ ### Express endpoint (`webhooks.ts`)
2
+
3
+ ```ts
4
+ import express from "express";
5
+ import { createPublicKey, verify, constants } from "node:crypto";
6
+
7
+ // Finrock's webhook signing key (https://apidocs.finrock.io/guides/webhooks)
8
+ const FINROCK_WEBHOOK_KEY = createPublicKey(`-----BEGIN PUBLIC KEY-----
9
+ MIIBnzANBgkqhkiG9w0BAQEFAAOCAYwAMIIBhwKCAX4Aq1O7ztZxzkgDCFlUnHvs
10
+ EBffqmlng9uEmNMCGbCpxB7991WiR4TRRVaTUrLxIzPWl79xT6R2Xzba1GqN2bin
11
+ sPBU/cmHEF5v1bDopZ7oHnU43tnlpH1kSOjtvTBORf6v6+9xh87OYtg4wXuwVzCq
12
+ ykXDRPIIzgPCSpA7l7DtPqhOpm0OVpUnVJQ5DgnsMfJMPkrrPTjRkpFZ1RbtqoLW
13
+ UI+bMWZAAKLEsh4Q+SRO7xiHj26gToD+/ZrEAbC/SOcy2ChDfpQ39XN/bYTu52j8
14
+ ndAS/R4qnffLGpmTcxr19UC+BPs5Wb5kx2ZJsGPQPpoPHaTBC1n3YwQlksgeHEvX
15
+ ZBsDoAvIXpGfBjUPzd8bvz5vyxbLYA4lSqp/Fw48NYgRRgfOWjmtqVI07jnXKxU2
16
+ kcQc7QkoFVDYw45DS14tkrsCOJjMF0qux2dL9Gr1z/Xdaod+SyeRqsTDYhbzaD+v
17
+ UkO6chtsIHmQwAF8jYajAR7uKROX+6f8ZIwYDco3AgMBAAE=
18
+ -----END PUBLIC KEY-----`);
19
+
20
+ export const webhooks = express.Router();
21
+
22
+ // express.raw keeps the exact bytes; never put express.json() in front of this route.
23
+ webhooks.post("/webhooks/finrock", express.raw({ type: "*/*", limit: "1mb" }), async (req, res) => {
24
+ const signature = req.header("x-signature") ?? "";
25
+ const raw: Buffer = Buffer.isBuffer(req.body) ? req.body : Buffer.alloc(0);
26
+ const ok = verify(
27
+ "sha512",
28
+ raw,
29
+ { key: FINROCK_WEBHOOK_KEY, padding: constants.RSA_PKCS1_PADDING },
30
+ Buffer.from(signature, "base64"),
31
+ );
32
+ if (!ok) return res.status(401).send("invalid signature");
33
+
34
+ const event = JSON.parse(raw.toString("utf8"));
35
+
36
+ // Deduplicate: retries resend the same event, and each status change is its own event.
37
+ const firstTime = await db.webhookEvents.insertIfAbsent(`${event.id}:${event.status}`);
38
+ if (!firstTime) return res.sendStatus(200);
39
+
40
+ if (event.type === "Deposit" && event.status === "Success") {
41
+ // Find the customer from the deposit address/label you created for them.
42
+ await creditDeposit(event); // your logic, in a transaction
43
+ } else if (event.type === "Withdraw") {
44
+ await db.withdrawals.updateBySequenceId(event.sequence_id, { status: event.status, hash: event.hash });
45
+ }
46
+ res.sendStatus(200);
47
+ });
48
+ ```
@@ -0,0 +1,34 @@
1
+ ---
2
+ title: Receive and verify Finrock webhooks
3
+ summary: Get real-time deposit and withdrawal events, verify the x-signature header, and process each event exactly once.
4
+ docs: guides/webhooks, reference/transaction-statuses, reference/transaction-types
5
+ client: false
6
+ ---
7
+
8
+ Finrock POSTs a JSON event to your endpoint whenever a deposit arrives or a withdrawal changes
9
+ status. Register the URL at **https://finrock.io/webhooks** (enter the URL, press **+**).
10
+
11
+ ### Rules
12
+
13
+ 1. **Verify every request.** The `x-signature` header is a base64 RSA-SHA512 (PKCS#1 v1.5)
14
+ signature of the raw body, made with Finrock's key. The public key is in the code below.
15
+ 2. **Verify the raw bytes.** Read the body *before* any JSON middleware touches it
16
+ (`express.raw()`, `await req.arrayBuffer()`, `await request.body()`, `php://input`).
17
+ 3. **Return HTTP 200 quickly.** Anything else, or a timeout, is retried up to 5 times
18
+ (after 30 s, 60 s, 90 s, 120 s, 180 s). Do slow work in a queue.
19
+ 4. **Be idempotent.** The same event can arrive more than once, and one transaction sends one
20
+ event per status change. Deduplicate on `id` + `status`.
21
+ 5. **Act on terminal statuses.** Credit a deposit only when `status` is `Success`. `Failed`,
22
+ `Rejected`, `Blocked`, `UserCancelled` and `AML_Blocked` are also final; everything else
23
+ (`Pending`, `Processing`, `Signing`, `SweepPending`, `NeedsApproval`, `AML_Pending`) is in progress.
24
+
25
+ ### Useful payload fields
26
+
27
+ `id`, `type` (`Deposit` or `Withdraw`), `asset` (e.g. `USDT_TRX`), `amount`, `amount_in_usd`,
28
+ `nett` (after fees, negative for withdrawals), `status`, `confirmations.{required,actual}`,
29
+ `hash`, `explorer_url`, `sequence_id` (your idempotency key for withdrawals),
30
+ `source_*`/`dest_*` (type, id, alias), `aml.cid`, `created_timestamp_utc`.
31
+
32
+ To test locally, expose your endpoint with a tunnel (for example `cloudflared` or `ngrok`) and
33
+ register the tunnel URL. The `verify_webhook_signature` tool on this server can check a captured
34
+ body and signature for you.
@@ -0,0 +1,46 @@
1
+ ### Endpoint (`finrock_webhook.php`)
2
+
3
+ ```php
4
+ <?php
5
+ require __DIR__ . '/db.php'; // your data layer
6
+
7
+ // Finrock's webhook signing key (https://apidocs.finrock.io/guides/webhooks)
8
+ const FINROCK_WEBHOOK_KEY = <<<PEM
9
+ -----BEGIN PUBLIC KEY-----
10
+ MIIBnzANBgkqhkiG9w0BAQEFAAOCAYwAMIIBhwKCAX4Aq1O7ztZxzkgDCFlUnHvs
11
+ EBffqmlng9uEmNMCGbCpxB7991WiR4TRRVaTUrLxIzPWl79xT6R2Xzba1GqN2bin
12
+ sPBU/cmHEF5v1bDopZ7oHnU43tnlpH1kSOjtvTBORf6v6+9xh87OYtg4wXuwVzCq
13
+ ykXDRPIIzgPCSpA7l7DtPqhOpm0OVpUnVJQ5DgnsMfJMPkrrPTjRkpFZ1RbtqoLW
14
+ UI+bMWZAAKLEsh4Q+SRO7xiHj26gToD+/ZrEAbC/SOcy2ChDfpQ39XN/bYTu52j8
15
+ ndAS/R4qnffLGpmTcxr19UC+BPs5Wb5kx2ZJsGPQPpoPHaTBC1n3YwQlksgeHEvX
16
+ ZBsDoAvIXpGfBjUPzd8bvz5vyxbLYA4lSqp/Fw48NYgRRgfOWjmtqVI07jnXKxU2
17
+ kcQc7QkoFVDYw45DS14tkrsCOJjMF0qux2dL9Gr1z/Xdaod+SyeRqsTDYhbzaD+v
18
+ UkO6chtsIHmQwAF8jYajAR7uKROX+6f8ZIwYDco3AgMBAAE=
19
+ -----END PUBLIC KEY-----
20
+ PEM;
21
+
22
+ $raw = file_get_contents('php://input'); // exact bytes
23
+ $signature = base64_decode($_SERVER['HTTP_X_SIGNATURE'] ?? '', true);
24
+
25
+ if ($signature === false || openssl_verify($raw, $signature, FINROCK_WEBHOOK_KEY, OPENSSL_ALGO_SHA512) !== 1) {
26
+ http_response_code(401);
27
+ exit('invalid signature');
28
+ }
29
+
30
+ $event = json_decode($raw, true);
31
+
32
+ // Deduplicate: retries resend the same event, and each status change is its own event.
33
+ if (!db_insert_webhook_event_if_absent($event['id'] . ':' . $event['status'])) {
34
+ http_response_code(200);
35
+ exit('ok');
36
+ }
37
+
38
+ if ($event['type'] === 'Deposit' && $event['status'] === 'Success') {
39
+ credit_deposit($event); // your logic, in a transaction
40
+ } elseif ($event['type'] === 'Withdraw') {
41
+ db_update_withdrawal($event['sequence_id'] ?? null, $event['status'], $event['hash'] ?? null);
42
+ }
43
+
44
+ http_response_code(200);
45
+ echo 'ok';
46
+ ```
@@ -0,0 +1,55 @@
1
+ ### FastAPI endpoint (`webhooks.py`)
2
+
3
+ ```bash
4
+ pip install cryptography
5
+ ```
6
+
7
+ ```python
8
+ import base64
9
+ import binascii
10
+ import json
11
+
12
+ from cryptography.exceptions import InvalidSignature
13
+ from cryptography.hazmat.primitives import hashes, serialization
14
+ from cryptography.hazmat.primitives.asymmetric import padding
15
+ from fastapi import APIRouter, Request, Response
16
+
17
+ from mydb import db, credit_deposit # your data layer
18
+
19
+ # Finrock's webhook signing key (https://apidocs.finrock.io/guides/webhooks)
20
+ FINROCK_WEBHOOK_KEY = serialization.load_pem_public_key(b"""-----BEGIN PUBLIC KEY-----
21
+ MIIBnzANBgkqhkiG9w0BAQEFAAOCAYwAMIIBhwKCAX4Aq1O7ztZxzkgDCFlUnHvs
22
+ EBffqmlng9uEmNMCGbCpxB7991WiR4TRRVaTUrLxIzPWl79xT6R2Xzba1GqN2bin
23
+ sPBU/cmHEF5v1bDopZ7oHnU43tnlpH1kSOjtvTBORf6v6+9xh87OYtg4wXuwVzCq
24
+ ykXDRPIIzgPCSpA7l7DtPqhOpm0OVpUnVJQ5DgnsMfJMPkrrPTjRkpFZ1RbtqoLW
25
+ UI+bMWZAAKLEsh4Q+SRO7xiHj26gToD+/ZrEAbC/SOcy2ChDfpQ39XN/bYTu52j8
26
+ ndAS/R4qnffLGpmTcxr19UC+BPs5Wb5kx2ZJsGPQPpoPHaTBC1n3YwQlksgeHEvX
27
+ ZBsDoAvIXpGfBjUPzd8bvz5vyxbLYA4lSqp/Fw48NYgRRgfOWjmtqVI07jnXKxU2
28
+ kcQc7QkoFVDYw45DS14tkrsCOJjMF0qux2dL9Gr1z/Xdaod+SyeRqsTDYhbzaD+v
29
+ UkO6chtsIHmQwAF8jYajAR7uKROX+6f8ZIwYDco3AgMBAAE=
30
+ -----END PUBLIC KEY-----""")
31
+
32
+ router = APIRouter()
33
+
34
+
35
+ @router.post("/webhooks/finrock")
36
+ async def finrock_webhook(request: Request):
37
+ raw = await request.body() # exact bytes; don't use request.json() before verifying
38
+ try:
39
+ signature = base64.b64decode(request.headers.get("x-signature", ""), validate=True)
40
+ FINROCK_WEBHOOK_KEY.verify(signature, raw, padding.PKCS1v15(), hashes.SHA512())
41
+ except (InvalidSignature, binascii.Error, ValueError):
42
+ return Response("invalid signature", status_code=401)
43
+
44
+ event = json.loads(raw)
45
+
46
+ # Deduplicate: retries resend the same event, and each status change is its own event.
47
+ if not db.insert_webhook_event_if_absent(f"{event['id']}:{event['status']}"):
48
+ return Response("ok")
49
+
50
+ if event["type"] == "Deposit" and event["status"] == "Success":
51
+ credit_deposit(event) # your logic, in a transaction
52
+ elif event["type"] == "Withdraw":
53
+ db.update_withdrawal(event.get("sequence_id"), status=event["status"], tx_hash=event.get("hash"))
54
+ return Response("ok")
55
+ ```
@@ -0,0 +1,35 @@
1
+ ### Payouts (`payouts.ts`)
2
+
3
+ ```ts
4
+ import { finrock } from "@/lib/finrock";
5
+
6
+ const TERMINAL = new Set(["Success", "Failed", "Rejected", "Blocked", "UserCancelled", "AML_Blocked"]);
7
+
8
+ export async function sendPayout(p: {
9
+ payoutId: string; // your unique ID; becomes Finrock's sequence_id
10
+ customerId: string;
11
+ asset: string; // e.g. "USDT_TRX"
12
+ amount: number; // native units
13
+ address: string;
14
+ tag?: string; // XRP / XLM destination tag
15
+ }) {
16
+ const tx = await finrock("POST", "/dac/v1/transaction", {
17
+ asset: p.asset,
18
+ amount: p.amount,
19
+ source_type: "INTERNAL",
20
+ destination_type: "EXTERNAL",
21
+ destination_id: p.address,
22
+ ...(p.tag ? { destination_tag: p.tag } : {}),
23
+ sequence_id: p.payoutId, // safe to retry: same ID returns the same transaction
24
+ aml_cid: p.customerId,
25
+ memo: `payout-${p.payoutId}`,
26
+ });
27
+ await db.payouts.update(p.payoutId, { finrockId: tx.id, status: tx.status });
28
+ return tx;
29
+ }
30
+
31
+ export const isTerminal = (status: string) => TERMINAL.has(status);
32
+ ```
33
+
34
+ Prefer webhooks (`Withdraw` events carry your `sequence_id`) to update the payout status.
35
+ If you must poll, back off from 5 s to 60 s and stop at a terminal status.
@@ -0,0 +1,35 @@
1
+ ### Payouts (`payouts.ts`)
2
+
3
+ ```ts
4
+ import { finrock } from "./finrock";
5
+
6
+ const TERMINAL = new Set(["Success", "Failed", "Rejected", "Blocked", "UserCancelled", "AML_Blocked"]);
7
+
8
+ export async function sendPayout(p: {
9
+ payoutId: string; // your unique ID; becomes Finrock's sequence_id
10
+ customerId: string;
11
+ asset: string; // e.g. "USDT_TRX"
12
+ amount: number; // native units
13
+ address: string;
14
+ tag?: string; // XRP / XLM destination tag
15
+ }) {
16
+ const tx = await finrock("POST", "/dac/v1/transaction", {
17
+ asset: p.asset,
18
+ amount: p.amount,
19
+ source_type: "INTERNAL",
20
+ destination_type: "EXTERNAL",
21
+ destination_id: p.address,
22
+ ...(p.tag ? { destination_tag: p.tag } : {}),
23
+ sequence_id: p.payoutId, // safe to retry: same ID returns the same transaction
24
+ aml_cid: p.customerId,
25
+ memo: `payout-${p.payoutId}`,
26
+ });
27
+ await db.payouts.update(p.payoutId, { finrockId: tx.id, status: tx.status });
28
+ return tx;
29
+ }
30
+
31
+ export const isTerminal = (status: string) => TERMINAL.has(status);
32
+ ```
33
+
34
+ Prefer webhooks (`Withdraw` events carry your `sequence_id`) to update the payout status.
35
+ If you must poll, back off from 5 s to 60 s and stop at a terminal status.
@@ -0,0 +1,32 @@
1
+ ---
2
+ title: Send crypto payouts and withdrawals
3
+ summary: Validate the address, create an idempotent transaction, and track it to a terminal status.
4
+ docs: guides/send-transaction, api-reference/transactions/create-transaction, api-reference/transactions/estimate-fee, api-reference/wallets/validate-address, reference/transaction-statuses, reference/transaction-types, reference/idempotency, guides/ad-hoc-addresses
5
+ client: true
6
+ ---
7
+
8
+ ### Flow
9
+
10
+ 1. **Validate the destination** with *Validate Address* before accepting it from a user.
11
+ 2. **Optionally estimate the fee** with *Estimate Fee* and show it to the user.
12
+ 3. **Create the transaction** with `POST /dac/v1/transaction`:
13
+ - `asset`, `amount` (native units, up to 8 decimals)
14
+ - `source_type`: `INTERNAL` (your wallet) or `EXCHANGE`
15
+ - `destination_type`: `EXTERNAL` for a customer's address (`INTERNAL`, `SUBACCOUNT`, `EXCHANGE` for your own)
16
+ - `destination_id`: the address; add `destination_tag` for XRP/XLM
17
+ - `sequence_id`: **always set this** to your own payout ID. If you retry with the same
18
+ `sequence_id`, Finrock returns the existing transaction instead of paying twice.
19
+ - `aml_cid`: your customer ID; required when AML screening is enabled.
20
+ 4. **Track it** by webhook (preferred) or by polling the transaction with backoff
21
+ (5 s doubling to 60 s) until a terminal status:
22
+ `Success`, `Failed`, `Rejected`, `Blocked`, `UserCancelled`, `AML_Blocked`.
23
+ `NeedsApproval` and `AML_Pending` mean wait; never resubmit with a new `sequence_id`.
24
+
25
+ ### Before going live
26
+
27
+ - Withdrawals to non-whitelisted addresses need *Ad-Hoc Addresses* enabled, and withdrawals to
28
+ smart contracts are blocked by default (see *Contract Addresses*).
29
+ - Debit the user's balance in the same database transaction that records the payout, and
30
+ refund it only on a terminal failure status.
31
+ - Bulk payouts on UTXO chains (BTC etc.) can use `destinations: [{ address, amount }, ...]`
32
+ instead of `destination_id`.
@@ -0,0 +1,37 @@
1
+ ### Payouts (`payouts.php`)
2
+
3
+ ```php
4
+ <?php
5
+ require_once __DIR__ . '/finrock.php';
6
+ require_once __DIR__ . '/db.php'; // your data layer
7
+
8
+ const FINROCK_TERMINAL = ['Success', 'Failed', 'Rejected', 'Blocked', 'UserCancelled', 'AML_Blocked'];
9
+
10
+ function send_payout(string $payoutId, string $customerId, string $asset, float $amount, string $address, ?string $tag = null): array
11
+ {
12
+ $body = [
13
+ 'asset' => $asset, // e.g. "USDT_TRX"
14
+ 'amount' => $amount, // native units
15
+ 'source_type' => 'INTERNAL',
16
+ 'destination_type' => 'EXTERNAL',
17
+ 'destination_id' => $address,
18
+ 'sequence_id' => $payoutId, // safe to retry: same ID returns the same transaction
19
+ 'aml_cid' => $customerId,
20
+ 'memo' => "payout-$payoutId",
21
+ ];
22
+ if ($tag !== null) {
23
+ $body['destination_tag'] = $tag; // XRP / XLM
24
+ }
25
+ $tx = finrock('POST', '/dac/v1/transaction', $body);
26
+ db_update_payout($payoutId, ['finrock_id' => $tx['id'], 'status' => $tx['status']]);
27
+ return $tx;
28
+ }
29
+
30
+ function is_terminal(string $status): bool
31
+ {
32
+ return in_array($status, FINROCK_TERMINAL, true);
33
+ }
34
+ ```
35
+
36
+ Prefer webhooks (`Withdraw` events carry your `sequence_id`) to update the payout status.
37
+ If you must poll, back off from 5 s to 60 s and stop at a terminal status.
@@ -0,0 +1,33 @@
1
+ ### Payouts (`payouts.py`)
2
+
3
+ ```python
4
+ from finrock import finrock
5
+ from mydb import db # your data layer
6
+
7
+ TERMINAL = {"Success", "Failed", "Rejected", "Blocked", "UserCancelled", "AML_Blocked"}
8
+
9
+
10
+ def send_payout(payout_id: str, customer_id: str, asset: str, amount: float, address: str, tag: str | None = None):
11
+ body = {
12
+ "asset": asset, # e.g. "USDT_TRX"
13
+ "amount": amount, # native units
14
+ "source_type": "INTERNAL",
15
+ "destination_type": "EXTERNAL",
16
+ "destination_id": address,
17
+ "sequence_id": payout_id, # safe to retry: same ID returns the same transaction
18
+ "aml_cid": customer_id,
19
+ "memo": f"payout-{payout_id}",
20
+ }
21
+ if tag:
22
+ body["destination_tag"] = tag # XRP / XLM
23
+ tx = finrock("POST", "/dac/v1/transaction", body)
24
+ db.update_payout(payout_id, finrock_id=tx["id"], status=tx["status"])
25
+ return tx
26
+
27
+
28
+ def is_terminal(status: str) -> bool:
29
+ return status in TERMINAL
30
+ ```
31
+
32
+ Prefer webhooks (`Withdraw` events carry your `sequence_id`) to update the payout status.
33
+ If you must poll, back off from 5 s to 60 s and stop at a terminal status.
package/package.json CHANGED
@@ -1,6 +1,57 @@
1
1
  {
2
2
  "name": "@finrock/mcp",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "mcpName": "io.finrock/mcp",
5
+ "description": "MCP server that helps AI agents integrate Finrock crypto payments",
6
+ "scripts": {
7
+ "build": "tsc -p tsconfig.build.json",
8
+ "prepublishOnly": "npm run build",
9
+ "typecheck": "tsc --noEmit",
10
+ "test": "vitest run",
11
+ "start": "node dist/http.js",
12
+ "start:stdio": "node dist/stdio.js",
13
+ "dev": "tsx watch src/http.ts"
14
+ },
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/gofinrock/mcp.git"
18
+ },
19
+ "keywords": [
20
+ "mcp",
21
+ "finrock",
22
+ "crypto",
23
+ "payments"
24
+ ],
25
+ "author": "Finrock",
26
+ "license": "SEE LICENSE IN LICENSE",
27
+ "bugs": {
28
+ "url": "https://github.com/gofinrock/mcp/issues"
29
+ },
30
+ "homepage": "https://github.com/gofinrock/mcp#readme",
31
+ "dependencies": {
32
+ "@modelcontextprotocol/sdk": "^1.32.1",
33
+ "express": "^5.3.0",
34
+ "zod": "^4.6.7"
35
+ },
36
+ "devDependencies": {
37
+ "@types/express": "^5.0.6",
38
+ "@types/node": "^26.6.5",
39
+ "tsx": "^4.23.15",
40
+ "typescript": "^7.0.2",
41
+ "vitest": "^5.0.5"
42
+ },
43
+ "type": "module",
44
+ "bin": {
45
+ "finrock-mcp": "dist/stdio.js"
46
+ },
47
+ "files": [
48
+ "dist",
49
+ "guides"
50
+ ],
51
+ "engines": {
52
+ "node": ">=22"
53
+ },
54
+ "exports": {
55
+ ".": "./dist/index.js"
56
+ }
57
+ }