@venlyfinance/settlement-mcp 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.
@@ -0,0 +1,177 @@
1
+ /**
2
+ * Domain types + the injectable VenlyClient interface.
3
+ *
4
+ * These shapes are a minimal projection of the vendored OpenAPI specs at
5
+ * projects/venly-docs-rebuild/api-reference/finance.yaml (servers:
6
+ * https://api.venlyfinance.com/api/v1) and fundflow.yaml (servers:
7
+ * https://api-fundflow.venly.io). Only the fields the tools actually read or
8
+ * echo are modeled. Fields are intentionally loose (optional) because this is a
9
+ * thin wrapper, not a full SDK.
10
+ *
11
+ * TRANSPORT NOTE: the bundled HttpVenlyClient is a deliberately minimal fetch
12
+ * transport (see client/http-client.ts). A future release replaces it with a
13
+ * thin adapter over `@venlyfinance/sdk` with no change to this interface;
14
+ * until then the minimal transport is what ships. When that lands, replace
15
+ * HttpVenlyClient with a thin adapter over it and delete the vendored transport.
16
+ */
17
+ /** Ramp request status flow, per fundflow.yaml overview. */
18
+ export type RampStatus = "AWAITING_APPROVAL" | "AWAITING_FUNDS" | "PROCESSING" | "SUCCEEDED" | "FAILED" | "BLOCKED" | "DENIED" | "REJECTED" | "CANCELLED";
19
+ export type RampType = "ON_RAMP" | "OFF_RAMP";
20
+ /** Simplified ramp request for list views (fundflow RampRequestListItem). */
21
+ export interface RampRequestListItem {
22
+ id: string;
23
+ paymentReference?: string;
24
+ rampType?: RampType;
25
+ status?: RampStatus;
26
+ fiatAmount?: number;
27
+ fiatCurrency?: string;
28
+ cryptoAmount?: number;
29
+ cryptoCurrency?: string;
30
+ createdAt?: string;
31
+ createdBy?: string;
32
+ }
33
+ /** Full ramp request detail (fundflow RampRequestDto). `version` drives the
34
+ * four-eyes optimistic-locking approve/reject calls. */
35
+ export interface RampRequestDto {
36
+ id: string;
37
+ companyId?: string;
38
+ companyName?: string;
39
+ rampType?: RampType;
40
+ status?: RampStatus;
41
+ fiatAmount?: number;
42
+ fiatNetAmount?: number;
43
+ cryptoAmount?: number;
44
+ fiatFeeAmount?: number;
45
+ exchangeRate?: number;
46
+ feePercentage?: number;
47
+ paymentReference?: string;
48
+ paymentReceived?: boolean;
49
+ blockchainTransactionHash?: string;
50
+ createdAt?: string;
51
+ createdBy?: string;
52
+ version?: number;
53
+ }
54
+ /** Finance Account (finance getAccount). */
55
+ export interface Account {
56
+ id: string;
57
+ status?: string;
58
+ reference?: string;
59
+ createdAt?: string;
60
+ updatedAt?: string;
61
+ [key: string]: unknown;
62
+ }
63
+ /** Finance VirtualBankAccount. `referenceCode` is the reconciliation key. */
64
+ export interface VirtualBankAccount {
65
+ id: string;
66
+ accountId?: string;
67
+ bankAccountType?: string;
68
+ name?: string;
69
+ status?: string;
70
+ currency?: string;
71
+ targetCryptocurrency?: string;
72
+ iban?: string;
73
+ bic?: string;
74
+ bankName?: string;
75
+ beneficiaryName?: string;
76
+ referenceCode?: string;
77
+ createdAt?: string;
78
+ updatedAt?: string;
79
+ }
80
+ /** Finance Transfer (finance getTransfer). */
81
+ export interface Transfer {
82
+ id: string;
83
+ status?: string;
84
+ fiatAmount?: string | number;
85
+ fiatCurrency?: string;
86
+ cryptocurrency?: string;
87
+ createdAt?: string;
88
+ [key: string]: unknown;
89
+ }
90
+ /** Finance Party (finance listParties). */
91
+ export interface Party {
92
+ id: string;
93
+ type?: string;
94
+ status?: string;
95
+ [key: string]: unknown;
96
+ }
97
+ /** A payment link (finance PaymentLink). */
98
+ export interface PaymentLink {
99
+ id: string;
100
+ accountId?: string;
101
+ paymentUrl?: string;
102
+ externalRef?: string;
103
+ status?: string;
104
+ [key: string]: unknown;
105
+ }
106
+ /**
107
+ * An observed incoming bank transaction on a vIBAN. This is operator- or
108
+ * bank-feed-supplied data (there is no list-vIBAN-transactions endpoint in the
109
+ * Release 1 specs), matched against a vIBAN referenceCode during reconciliation.
110
+ */
111
+ export interface ObservedBankTransaction {
112
+ /** The reference code carried in the payment (payment reference / remittance). */
113
+ referenceCode: string;
114
+ amount: number;
115
+ currency: string;
116
+ remitterName?: string;
117
+ valueDate?: string;
118
+ bankTransactionId?: string;
119
+ }
120
+ /** Query params for listing ramp requests (fundflow getAll). */
121
+ export interface ListRampRequestsParams {
122
+ rampType?: RampType;
123
+ status?: RampStatus;
124
+ fromDate?: string;
125
+ toDate?: string;
126
+ paymentReference?: string;
127
+ page?: number;
128
+ size?: number;
129
+ }
130
+ /** Body for the fiat transfer POST (finance CreateFiatTransferInput). */
131
+ export interface CreateFiatTransferInput {
132
+ receiverAccountId: string;
133
+ receiverExternalId?: string;
134
+ fiatAmount: string;
135
+ fiatCurrency: string;
136
+ cryptocurrency?: string;
137
+ description?: string;
138
+ merchantReference?: string;
139
+ }
140
+ /** Body for approve/reject (fundflow UpdateWithOptimisticLockingRequest). */
141
+ export interface OptimisticLockingBody {
142
+ version: number;
143
+ }
144
+ /** Body for the payment link POST (finance CreatePaymentLinkRequest). */
145
+ export interface CreatePaymentLinkRequest {
146
+ inAmount: string;
147
+ inCurrency: string;
148
+ outCryptocurrency?: string;
149
+ redirectUrl?: string;
150
+ externalRef?: string;
151
+ metadata?: Record<string, string>;
152
+ }
153
+ /**
154
+ * The injectable Venly transport. HttpVenlyClient is the real fetch-based
155
+ * implementation; tests inject a mock. The MCP layer depends ONLY on this
156
+ * interface, never on a concrete transport, which is what makes the fail-closed
157
+ * write path testable without a network.
158
+ */
159
+ export interface VenlyClient {
160
+ listRampRequests(params?: ListRampRequestsParams): Promise<RampRequestListItem[]>;
161
+ getRampRequest(id: string): Promise<RampRequestDto>;
162
+ getAccount(accountId: string): Promise<Account>;
163
+ listVirtualBankAccounts(accountId: string): Promise<VirtualBankAccount[]>;
164
+ getTransfer(accountId: string, transferId: string): Promise<Transfer>;
165
+ listParties(params?: {
166
+ page?: number;
167
+ size?: number;
168
+ }): Promise<Party[]>;
169
+ getSupportedChains(): Promise<unknown[]>;
170
+ getFiatCurrencies(): Promise<unknown[]>;
171
+ getCryptocurrencies(): Promise<unknown[]>;
172
+ getCompanyFees(): Promise<unknown>;
173
+ createFiatTransfer(senderAccountId: string, body: CreateFiatTransferInput): Promise<Transfer>;
174
+ approveRampRequest(id: string, body: OptimisticLockingBody): Promise<RampRequestDto>;
175
+ rejectRampRequest(id: string, body: OptimisticLockingBody): Promise<RampRequestDto>;
176
+ createPaymentLink(accountId: string, body: CreatePaymentLinkRequest): Promise<PaymentLink>;
177
+ }
package/dist/types.js ADDED
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Domain types + the injectable VenlyClient interface.
3
+ *
4
+ * These shapes are a minimal projection of the vendored OpenAPI specs at
5
+ * projects/venly-docs-rebuild/api-reference/finance.yaml (servers:
6
+ * https://api.venlyfinance.com/api/v1) and fundflow.yaml (servers:
7
+ * https://api-fundflow.venly.io). Only the fields the tools actually read or
8
+ * echo are modeled. Fields are intentionally loose (optional) because this is a
9
+ * thin wrapper, not a full SDK.
10
+ *
11
+ * TRANSPORT NOTE: the bundled HttpVenlyClient is a deliberately minimal fetch
12
+ * transport (see client/http-client.ts). A future release replaces it with a
13
+ * thin adapter over `@venlyfinance/sdk` with no change to this interface;
14
+ * until then the minimal transport is what ships. When that lands, replace
15
+ * HttpVenlyClient with a thin adapter over it and delete the vendored transport.
16
+ */
17
+ export {};
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "@venlyfinance/settlement-mcp",
3
+ "version": "0.1.0",
4
+ "description": "Venly Settlement MCP server. Human-gated operator surface over the Venly Finance and Fundflow APIs, plus an x402 machine-to-machine quote stub. Read-only by default, write tools fail closed.",
5
+ "type": "module",
6
+ "bin": {
7
+ "venly-settlement-mcp": "dist/index.js"
8
+ },
9
+ "main": "dist/index.js",
10
+ "files": [
11
+ "dist",
12
+ "skills",
13
+ "README.md"
14
+ ],
15
+ "scripts": {
16
+ "build": "tsc -p tsconfig.json",
17
+ "start": "node dist/index.js",
18
+ "test": "node --import tsx --test test/*.test.ts",
19
+ "typecheck": "tsc -p tsconfig.json --noEmit"
20
+ },
21
+ "engines": {
22
+ "node": ">=18"
23
+ },
24
+ "dependencies": {
25
+ "@modelcontextprotocol/sdk": "^1.29.0",
26
+ "zod": "^3.23.8"
27
+ },
28
+ "devDependencies": {
29
+ "@types/node": "^22.10.0",
30
+ "tsx": "^4.19.2",
31
+ "typescript": "^5.7.2"
32
+ },
33
+ "license": "MIT",
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "git+https://github.com/timdierckxsens/venly-sdk.git",
37
+ "directory": "settlement-mcp"
38
+ },
39
+ "homepage": "https://github.com/timdierckxsens/venly-sdk/tree/main/settlement-mcp#readme",
40
+ "bugs": {
41
+ "url": "https://github.com/timdierckxsens/venly-sdk/issues"
42
+ },
43
+ "keywords": [
44
+ "mcp",
45
+ "model-context-protocol",
46
+ "stablecoin",
47
+ "settlement",
48
+ "payments",
49
+ "x402",
50
+ "venly",
51
+ "fintech",
52
+ "eur",
53
+ "viban"
54
+ ]
55
+ }
@@ -0,0 +1,39 @@
1
+ # Skill: Walk a ramp through four-eyes approval
2
+
3
+ Move a ramp request from `AWAITING_APPROVAL` to `AWAITING_FUNDS` (approve) or to
4
+ `REJECTED` (reject), preserving four-eyes control.
5
+
6
+ ## When to use
7
+
8
+ A Company Manager created a ramp request. A Company Admin (a different identity)
9
+ must review and approve or reject it.
10
+
11
+ ## Tools
12
+
13
+ - `list_ramp_requests` (read)
14
+ - `get_ramp_request` (read)
15
+ - `approve_ramp_request` (write, disarmed by default)
16
+ - `reject_ramp_request` (write, disarmed by default)
17
+
18
+ ## Steps
19
+
20
+ 1. `list_ramp_requests` with `status: "AWAITING_APPROVAL"` to find pending
21
+ requests. Note `id` and `createdBy`.
22
+ 2. `get_ramp_request` for the chosen `id`. Read the amounts, the `paymentReference`,
23
+ and the `version`. The `version` is required for the optimistic-locking write.
24
+ 3. Decide. Four-eyes: the approving identity must differ from `createdBy`. The
25
+ Fundflow API enforces this; the tool surfaces the state, it does not bypass it.
26
+ 4. Call `approve_ramp_request` (or `reject_ramp_request`) with `id` and the
27
+ `version` from step 2.
28
+ - By default the tool returns a dry-run object showing the exact POST it would
29
+ send. Review it.
30
+ - To execute live, all three must hold: `confirm: true`, `VENLY_MCP_LIVE=1`,
31
+ and credentials present. Arming is a deliberate human decision.
32
+ 5. On a version conflict (HTTP 409) in live mode, re-fetch with `get_ramp_request`
33
+ to get the new `version` and retry.
34
+
35
+ ## Notes
36
+
37
+ - Approve transitions `AWAITING_APPROVAL` to `AWAITING_FUNDS`. Reject transitions
38
+ to `REJECTED`.
39
+ - The dry-run is the safe default. Read the request body before arming.
@@ -0,0 +1,44 @@
1
+ # Skill: Create and track a payment link
2
+
3
+ Stand up a fiat-to-crypto payment link for an account, hand the URL to a payer,
4
+ and follow the payment through to settlement.
5
+
6
+ ## When to use
7
+
8
+ An operator wants to collect a fiat payment that settles as stablecoin into an
9
+ account's wallet, without building a checkout: invoicing, one-off B2B
10
+ collections, top-ups.
11
+
12
+ ## Tools
13
+
14
+ - `get_account` (read)
15
+ - `list_virtual_bank_accounts` (read)
16
+ - `create_payment_link` (write, disarmed by default)
17
+ - `get_transfer` (read)
18
+
19
+ ## Steps
20
+
21
+ 1. `get_account` for the collecting account. Confirm `status: "ACTIVE"` - an
22
+ unverified or suspended account cannot collect.
23
+ 2. Optional context: `list_virtual_bank_accounts` shows the account's existing
24
+ collection surfaces (IBAN + referenceCode); a payment link is the hosted
25
+ alternative for payers who won't do a bank transfer.
26
+ 3. `create_payment_link` with the `accountId` and an `externalRef` your own
27
+ system can reconcile on later.
28
+ - By default the tool returns a dry-run object with the exact POST it would
29
+ send. Review it.
30
+ - Live execution needs all three: `confirm: true`, `VENLY_MCP_LIVE=1`, and
31
+ credentials present.
32
+ 4. Share the returned `paymentUrl` with the payer. Status starts at `CREATED` /
33
+ `PENDING_PAYMENT`.
34
+ 5. After payment, the received fiat converts and lands on-chain. Track the
35
+ resulting movement with `get_transfer` and reconcile on your `externalRef`.
36
+
37
+ ## Notes
38
+
39
+ - Payment links expire (`expiresAt`); a link that was never paid ends at
40
+ `EXPIRED`, not `FAILED`.
41
+ - Statuses walk `CREATED → PENDING_PAYMENT → PAYMENT_RECEIVED → CONVERTING →
42
+ MINTING → COMPLETED`, with `FAILED`, `CANCELLED`, `REFUNDING`, `REFUNDED` as
43
+ exits. Only treat `COMPLETED` as settled.
44
+ - The dry-run is the safe default. Read the request body before arming.
@@ -0,0 +1,40 @@
1
+ # Skill: Reconcile a EUR payment by referenceCode
2
+
3
+ Match an incoming EUR bank payment on a virtual IBAN (vIBAN) to the vIBAN that
4
+ issued its reference code, using only read tools. No mutation.
5
+
6
+ ## When to use
7
+
8
+ An operator sees an incoming EUR transfer (from a bank feed, statement, or
9
+ notification) and needs to know which account and vIBAN it belongs to, and
10
+ whether the expected funds have arrived.
11
+
12
+ ## Tools
13
+
14
+ - `list_virtual_bank_accounts` (read)
15
+ - `reconcile_by_reference_code` (read, composite)
16
+
17
+ ## Steps
18
+
19
+ 1. Identify the settlement `accountId` in question.
20
+ 2. Collect the observed incoming transactions. Each needs at least a
21
+ `referenceCode`, `amount`, and `currency` (add `remitterName`, `valueDate`,
22
+ `bankTransactionId` when available).
23
+ 3. Call `reconcile_by_reference_code` with `accountId`, the target
24
+ `referenceCode`, and the `transactions` array. The tool fetches the account's
25
+ vIBANs and matches.
26
+ 4. Read the result:
27
+ - `matched: true` plus a `virtualBankAccount` and `matchedTransactions`: the
28
+ payment is reconciled. `totalAmount` is the summed value.
29
+ - `matched: false` with a `virtualBankAccount` but no transactions: the vIBAN
30
+ exists, funds have not arrived. Awaiting funds.
31
+ - `matched: false` with `virtualBankAccount: null` but transactions present:
32
+ the payment references a code no vIBAN on this account carries. Possible
33
+ misdirected payment, investigate.
34
+
35
+ ## Notes
36
+
37
+ - There is no list-vIBAN-transactions endpoint in Release 1, so the transactions
38
+ are supplied by the operator or an upstream bank feed. The tool does the
39
+ matching, not the fetching of bank transactions.
40
+ - Read-only. This skill never approves, transfers, or mutates anything.
@@ -0,0 +1,39 @@
1
+ # Skill: Stage and confirm a transfer
2
+
3
+ Prepare a fiat-to-crypto transfer, review the exact request, then execute it only
4
+ behind the explicit confirm and arming gate.
5
+
6
+ ## When to use
7
+
8
+ An operator needs to move funds from a sender account to a receiver, converting
9
+ fiat to crypto.
10
+
11
+ ## Tools
12
+
13
+ - `get_account` (read)
14
+ - `stage_transfer` (write, disarmed by default)
15
+ - `get_transfer` (read)
16
+
17
+ ## Steps
18
+
19
+ 1. `get_account` for the `senderAccountId` to confirm it is active.
20
+ 2. Stage the transfer: call `stage_transfer` with `senderAccountId`,
21
+ `receiverAccountId`, `fiatAmount` (decimal string), `fiatCurrency`, and
22
+ optionally `cryptocurrency`, `description`, `merchantReference`. Omit
23
+ `confirm` (or set it false).
24
+ 3. The tool returns a dry-run object: the exact `POST /accounts/{senderAccountId}/transfers/fiat`
25
+ body it would send, plus the gate decision. Review the amount, currency, and
26
+ receiver.
27
+ 4. To execute live, re-call with `confirm: true` AND the server armed with
28
+ `VENLY_MCP_LIVE=1` AND credentials present. If any leg is missing, the tool
29
+ dry-runs again and does not touch the API.
30
+ 5. After a live execution, use `get_transfer` with the returned account and
31
+ transfer id to confirm status.
32
+
33
+ ## Notes
34
+
35
+ - Dry-run first is the default and the safe path. Confirm and arming are separate,
36
+ deliberate steps.
37
+ - Idempotency keys are handled by the transport on live POSTs.
38
+ - The fiat transfer endpoint is documented in the rebuild specs but marked a stub
39
+ pending live annotation. Cross-check the live schema before a production run.
@@ -0,0 +1,39 @@
1
+ # Skill: Read an x402 settlement quote
2
+
3
+ Get an HTTP-402-shaped quote for a settlement action and understand what each
4
+ field commits to, without moving any funds.
5
+
6
+ ## When to use
7
+
8
+ An agent (or an operator evaluating agent payments) wants to know what a
9
+ machine-to-machine settlement over the x402 rail would cost and where it would
10
+ pay, before any decision to execute anything.
11
+
12
+ ## Tools
13
+
14
+ - `quote_x402_payment` (stub - never executes, never calls a facilitator)
15
+ - `get_reference_data` (read; chains and currencies for context)
16
+
17
+ ## Steps
18
+
19
+ 1. Call `quote_x402_payment` with the settlement action you are pricing.
20
+ 2. Read the returned `PaymentRequirements`-shaped quote:
21
+ - `price`: the amount the counterparty would require.
22
+ - `asset`: the settlement asset (typically USDC).
23
+ - `payTo`: the receiving address.
24
+ - `chain`: where settlement would occur (CAIP-2 style network reference).
25
+ 3. Cross-check `chain` and `asset` against `get_reference_data` - a quote on an
26
+ unsupported chain/currency pair is a configuration error, not an offer.
27
+ 4. Stop. This tool states Venly's position on the x402 rail; it does not settle.
28
+
29
+ ## Notes
30
+
31
+ - Position of record: the MCP is the human-gated operator surface; x402 is the
32
+ machine-to-machine rail. This server ships the quote shape so integrators can
33
+ build against it today.
34
+ - Live x402 settlement requires a facilitator decision (who submits the signed
35
+ payment on-chain, under whose licence) that deliberately sits outside this
36
+ server. No `confirm` flag, no env var, and no credential arms this tool into
37
+ executing - it has no execution path.
38
+ - The x402 pattern: a server answers `402 Payment Required` with these fields;
39
+ the payer signs (EIP-3009/Permit2 style) and a facilitator submits on-chain.