# Fystack documentation > Fystack is a self-custody digital asset platform. Its API lets you create and manage MPC and HD wallets, receive deposits, send withdrawals, sign Web3 transactions, enforce programmable policies and multi-party approvals, and receive webhooks, across EVM chains, Solana, Tron, Bitcoin and more. --- # Product Documentation Learn how to use the Fystack dashboard to manage workspaces, wallets, users, and security settings across your organization. - [Workspace](https://docs.fystack.io/product/workspace.md): The top-level organizational unit in Fystack, an isolated environment for managing wallets, users, transactions, and settings. - [Network Management](https://docs.fystack.io/product/network.md): Configure network settings, monitor health status, and maintain data integrity for the blockchain networks you manage. - [User Management](https://docs.fystack.io/product/user-management.md): Manage workspace members, control access levels, and maintain operational security by assigning roles and permissions. - [Approval Group](https://docs.fystack.io/product/approval-group.md): A named set of workspace members plus the number of signatures required from them before a sensitive action takes effect. - [Security & 2FA](https://docs.fystack.io/product/security.md): Secure your Fystack account with Passkey and TOTP two-factor authentication. - [Withdrawal](https://docs.fystack.io/product/withdrawal.md): Send funds from a Fystack wallet to an external address through a multi-step form with risk screening and an approval queue. - [Web3 Sign](https://docs.fystack.io/product/web3-sign.md): Create signing requests through the SDK or API and review them in the dashboard. - [Gas Station](https://docs.fystack.io/product/gas-station.md): Fystack's built-in gas management hub, holding native tokens across multiple chains so wallets always have enough gas. - [Automation](https://docs.fystack.io/product/automation.md): Streamline crypto operations and reduce operational overhead with Fystack's automated treasury tooling. - [Policies](https://docs.fystack.io/product/policies.md): Set programmable guardrails that automatically allow, block, or route sensitive actions through approval in your workspace. - [Alerts](https://docs.fystack.io/product/alerts.md): Monitor wallet activity in real time and receive instant notifications through channels like Telegram. --- # Withdrawal Send funds from a Fystack wallet to an external address. Withdrawals go through a multi-step form, including address risk screening and category tagging, before entering an approval queue based on your workspace's signing threshold. ## Overview The withdrawal flow is designed around maker-checker controls. A **Proposer** or **Admin** initiates the withdrawal; one or more **Signers** must then approve it before it is broadcast on-chain. The number of required approvals is defined by the wallet's threshold configuration. ## How to Create a Withdrawal ### Step 1: Navigate to Your Wallet and Click Send In the left sidebar, click **Wallet**. Select the wallet you want to send from, then click the **Send** button in the top area of the wallet overview. _Image: Wallet overview showing the Send button highlighted_ ### Step 2: Fill in the Withdrawal Details A **Withdraw** modal opens with four required fields: 1. **Wallet**: the source wallet (pre-selected based on where you clicked Send) 2. **Asset**: the token and network to send (e.g. USDC on Base) 3. **Amount**: the amount to send; the available balance and estimated network fee are shown below the field 4. **Recipient Address**: type an address directly or select one from your **Address Book** _Image: Withdraw modal showing Wallet, Asset, Amount, and Recipient Address fields_ > **Note:** The **Network Fee** is deducted from the native gas token of the selected network, not from the asset being sent. Ensure your wallet holds enough of the gas token to cover the fee. ### Step 3: Review the Address Risk Analysis After selecting a recipient address, Fystack automatically performs an **Address Risk Analysis** powered by DD.xyz. The panel displays: - **Risk Score**: a percentage from 0% (low risk) to 100% (high risk) - **Current Status**: Low / Medium / High - **On-chain data**: total transactions, current balance, and date of first transaction _Image: Address Risk Analysis panel showing a 7.1% risk score and an Address Poisoning Attack Warning_ > **Note:** If an **Address Poisoning Attack Warning** appears, the recipient address has been flagged as a target of address poisoning, a scam where attackers send tiny transactions from look-alike addresses to trick you into copying the wrong address from your history. Double-check the full address character by character with your counterparty before proceeding. ### Step 4: Select Categories and Add Notes, then Click Withdraw Before submitting, tag the transaction for reporting purposes: - **Categories (Optional)**: assign one or more categories (e.g. MKT, Ops). You can create new categories with a custom name and color on the spot. - **Notes**: add a free-text note that will be visible to approvers and in the transaction history. An inline warning is shown reminding you to **verify address safety** before proceeding. Once satisfied, click the **Withdraw** button to submit the transaction for approval. _Image: Withdraw modal showing category selection, notes field, and the Withdraw button_ ### Step 5: Approval Status Once submitted, an **Approval Status** modal appears summarising the transaction details and listing the required approvers. Each approver shows a **Pending Approval** status until they act. If you are one of the required signers, you can **Approve** or **Cancel** the transaction directly from this modal. _Image: Approval Status modal listing pending approvers for the withdrawal_ > **Note:** The transaction is broadcast on-chain only after the required number of signers (as defined by the wallet threshold) have approved it. ### Step 6: Track the Withdrawal in the Withdrawals Tab Navigate to the wallet, then open the **Withdrawals** tab. Your new transaction appears at the top of the list with a **Pending Approval** badge. The wallet header also reflects the amount now **On Hold** until the threshold is met. Other approvers can click **Review** to open the Approval Status modal and cast their vote. _Image: Withdrawals tab showing the new 2 USDC transaction with Pending Approval status and funds on hold_ ### Step 7: Confirm in the Transactions Tab Once the required approvals are collected, switch to the **Transactions** tab in the same wallet. The withdrawal appears at the top with a **Completed** status, confirming it has been broadcast on-chain. _Image: Transactions tab showing the 2 USDC transfer with Completed status_ ### Step 8: Verify On-Chain via the Block Explorer Click the external link icon on the transaction row to open the transaction on **BaseScan** (or the relevant block explorer for your network). Here you can confirm: - **Status**: Success - **Transaction Hash**: the unique on-chain identifier - **Block**: the block number and confirmation by the sequencer - **Amount and recipient**: verify the correct amount reached the correct address _Image: BaseScan transaction detail page showing the 2 USDC transfer with Success status_ ## Withdrawal Statuses | Status | Meaning | | --- | --- | | Pending Approval | The transaction has been proposed and is awaiting the required number of signer approvals. Funds are placed on hold during this period. | | Completed | The threshold was met and the transaction was successfully broadcast and confirmed on-chain | | Cancelled | A signer or the proposer cancelled the transaction before the threshold was reached. Held funds are released back to the available balance. | ## Best Practices - Always verify the recipient address independently before submitting, on-chain transactions are irreversible - Use the **Address Book** to store and reuse trusted addresses, reducing the risk of manual entry errors - Take **Address Poisoning Attack Warnings** seriously, always confirm the full address with your counterparty through a separate channel - Add a **Note** describing the purpose of the withdrawal to help approvers make informed decisions and maintain a clear audit trail - Assign a **Category** to every withdrawal to keep your transaction reporting organised - After approval, cross-check the transaction on the block explorer to confirm the correct amount reached the correct address --- # Web3 Sign Sign messages, typed data, and transactions with your Fystack wallet. Requests can only be created through the SDK or API. See the [Web3 Sign API docs](https://docs.fystack.io/web3-sign.md) for endpoints and examples. ## Enable Web3 signing Web3 signing is disabled by default for each wallet. Open the wallet's **Settings**, enable **Web3 Signing**, then click **Update** and confirm the change before using the SDK or API. _Image: Wallet settings with the Web3 Signing toggle highlighted and disabled_ > **Warning: Unsafe signing.** Web3 signing can put your funds at risk. Only enable if you know what you are doing. Signing requests return **403 Forbidden** while Web3 Signing is disabled. ## Review pending requests Open **Approval** in the sidebar, then select **DApp Transaction**. Web3 signing requests awaiting approval appear here. _Image: Approval page with the DApp Transaction tab selected and a typed data signing request marked Pending Approval_ Click **Review** beside a request to inspect it before approving or rejecting it. Signing proceeds once the required approvals are met. --- # Approval Groups An **approval group** is a named set of workspace members plus the number of signatures required from them before a sensitive action takes effect. Each group carries three things: - **Members**: the workspace users allowed to approve on behalf of this group - **Threshold**: how many of those members must approve before the action proceeds - **Status**: whether the group itself has been approved and is available for use Every workspace ships with a **Default Approval Group**, created automatically with a threshold of 1. It reviews the creation of your other approval groups, so it exists before you need it. _Image: Approval Group tab under User Management showing the Default Approval Group with its threshold, status, and members_ ## Where Approval Groups Apply A group takes effect once you attach it to something. | Attached to | What it gates | | --- | --- | | A wallet | Only this group's members can approve withdrawals from the wallet, using the group's threshold. | | A policy | Creating, editing, or deleting the policy and its rules requires sign-off from this group before the change is applied. | | Another approval group | Creating a new group is itself an approval-gated action, reviewed by an existing group. | > **Note:** Only the workspace **Owner** can create an approval group. Other roles can be > added as members and can approve requests. See [User > Management](https://docs.fystack.io/product/user-management.md) for what each role can do. ## How to Create an Approval Group ### Step 1: Open the Approval Group Tab In the left sidebar, open **User Management**, then select the **Approval Group** tab. Existing groups are listed with their description, threshold, status, and member avatars. Click **+ Create Approval Group**. ### Step 2: Name the Group and Select Members Fill in the **Group Name**, and optionally a **Description**. Name it after the group's job, such as `Treasury Signers` or `Policy Reviewers`. Under **Members**, search by name or email, or filter by role. Each user shows their workspace role next to their name. Click a user to select them. _Image: Create Approval Group modal with group name, description, member selection, and approval threshold_ ### Step 3: Set the Approval Threshold Use the stepper to choose how many members must approve. The form restates your choice underneath, for example "Requires 2 of 2 selected member(s) to approve." > **Note:** The threshold cannot exceed the number of members you selected. A group with > 2 members has a maximum threshold of 2. To require 3 signatures, add a third > member first. Click **Create Group** when the members and threshold are correct. ### Step 4: Wait for the Creation Request to Be Approved Creating the group raises an approval request that goes to an existing approval group, normally the Default Approval Group. The new group appears in the list immediately, but its status stays **Pending** until that request is signed off. Reviewers are notified as soon as the request is raised. _Image: Notification panel showing an Approval Group Creation notification marked as Delivered_ > **Note:** Without this gate, one person could create a group containing only themselves, > attach it to a wallet, and approve their own withdrawals. ### Step 5: Review and Approve the Request Reviewers open **Approval** in the sidebar and select the **Approval Group Request** tab, which shows a count badge when items are waiting. Each row lists when the request was raised, who raised it, which group is reviewing it, the title, the approval type, and the status. Click **Review**. _Image: Approval page with the Approval Group Request tab listing one pending approval_group request_ The dialog shows who requested the change, a copyable request ID for your records, and the **Approval Progress** with each reviewer's individual status. Add an optional comment, then click **Approve** or **Reject**. Comments are capped at 500 characters. Use one when you reject, so the requester knows what to fix. _Image: Approval Group Creation review dialog showing request details, approval progress, and Approve or Reject actions_ Once the threshold is met, the group's status changes to **Approved** and it becomes selectable on wallets and policies. _Image: Approval Group tab showing the new group with an Approved status_ ## Inspecting a Group Open any group to see total members, the threshold, whether the group is active, and each member's approval status while a request is outstanding. The footer restates the rule and how many signatures are still missing. > **Note:** Membership changes are approval-gated in the same way. Adding a user to a > group raises its own request and needs sign-off before it applies, so nobody > can quietly add themselves to a group that guards a wallet. _Image: Approval Group Detail dialog showing members, threshold, active state, and per-member pending status_ ## Approval Group Statuses | Status | Meaning | | --- | --- | | Pending | The group has been proposed and is waiting on signatures from the reviewing group. It cannot be assigned to a wallet or policy yet. | | Approved | The group reached its reviewing group's threshold and is available for use across the workspace. | | Rejected | A reviewer declined the request and it did not take effect. Raise a new request to try again. | ## Assigning a Group to a Wallet ### At Wallet Creation In the **Create Wallet** dialog, expand **Advanced**. The **Approval Group (Optional)** dropdown lists your approved groups. Only that group's members can then approve withdrawals from this wallet. _Image: Create Wallet dialog with the Advanced section expanded, showing the Approval Group dropdown_ ### On an Existing Wallet Open the wallet, then the **Settings** tab. The wallet header shows the threshold in force. Two fields on this page work together with the group: 1. **Multi-Approval Threshold**: follows the assigned group's threshold. To change how many signatures a wallet requires, change the threshold on the group. 2. **Auto-Approval Limit**: withdrawals below this USD amount skip approval entirely. Set it to 0 so every withdrawal needs approval. Changing which group a wallet uses is itself approval-gated. The page names the group the wallet inherits and offers a **Request Approval** action, so no single admin can swap a strict group for a permissive one. _Image: Wallet Settings tab showing Multi-Approval Threshold, Auto-Approval Limit, and the Approval Group field with its inheritance notice_ ## Assigning a Group to a Policy Every policy names an approval group, which decides who reviews that policy's rules before they take effect. See [Policies](https://docs.fystack.io/product/policies.md) for what policies do and how rule conditions are evaluated. ### Choose the Governance Group In the policy wizard, the **Governance** section holds the **Approval Group** dropdown. Pick the group whose members should be responsible for this policy. _Image: Policy wizard showing the Governance section with the Approval Group dropdown_ ### Wait for Approval A newly created policy sits at **Pending** with an "Awaiting approval" banner, and its rules show as waiting for approval. The detail page names the approval group responsible. _Image: Policy detail page showing an Awaiting approval banner and the responsible approval group_ ### Review the Change Policy approvals show **Revision changes**, a field-by-field breakdown of what is being added or modified, including each rule's trigger action, effect, condition, and enabled state. This is the record of what you signed off on. _Image: Policy Creation review dialog showing revision changes with rule diffs and approval progress_ > **Note:** The same gate applies to later changes. Editing a rule, adding a rule, or > deleting the policy raises a new approval request, and nothing takes effect > until the group signs off. ## Approving a Withdrawal When a wallet has an approval group assigned, a withdrawal from that wallet enters the approval queue instead of going straight to signing. It collects signatures from the group's members until the threshold is reached, then proceeds to execution. Open the wallet and select the **Withdrawals** tab to see every request and where it stands. The header shows the threshold in force and a count of what is waiting. The balance panel separates **Available** from **On Hold**, so funds committed to pending withdrawals stay visible. _Image: Wallet Withdrawals tab showing pending approval, partially approved, and successful withdrawals alongside the threshold badge and on-hold balance_ | Status | Meaning | | --- | --- | | Pending Approval | The request is waiting and no member of the group has signed yet. | | You Approved | Your signature is recorded and the request is short of the threshold. The row states how many more approvals are needed. | | Success | The threshold was met and the withdrawal was signed and broadcast. | ### Tracking Who Has Signed Open a pending withdrawal to see its approval timeline: the amount, network, and recipient address being approved, then each member of the group with their decision and when they made it. Every signature is attributed to a named person, which is what makes the record useful when you need to answer an auditor or trace an unexpected transfer. _Image: Approval Status dialog showing withdrawal amount, network, recipient address, and a per-member approval timeline_ ### What Can Change the Path 1. The withdrawal value falls under the wallet's **Auto-Approval Limit**, in which case it bypasses multi-signature approval. 2. A policy rule matches with an **Allow** effect, which lets the withdrawal skip the approval queue. 3. A policy rule matches with a **Deny** effect, which blocks the withdrawal outright. Deny always wins over Allow. See [Withdrawal](https://docs.fystack.io/product/withdrawal.md) for the full request and execution flow, and [Policies](https://docs.fystack.io/product/policies.md) for how rules are evaluated. ## Best Practices - Give every production wallet an approval group with a threshold of at least 2, so no single compromised account can move funds - Size the group larger than the threshold, for example 3 members with a threshold of 2, so one person being unavailable does not freeze operations - Keep **Auto-Approval Limit** at 0 on wallets holding real balance, since any withdrawal below that amount is signed and sent without a second person seeing it - Keep the reviewing group for policies separate from the people who propose policy changes, which preserves the maker-checker split described in [User Management](https://docs.fystack.io/product/user-management.md) - Name groups after their responsibility rather than after individuals, so the group survives staff changes --- # Multi-chain Address Book A **multi-chain address book entry** is a named record holding every address a single counterparty uses across the chains it supports. Withdrawals, sweep tasks, and policy rules can each reference the same record. _Image: Address Book showing saved entries with their name, chain icons, address count, and last updated time_ > **Note:** Only the **Owner** and **Admin** roles can create, edit, or delete an address book entry. Every other role can select saved entries when building a withdrawal, a sweep task, or a policy rule. ## Creating an Entry Open **Address Book** from the sidebar and select **Create Entry**. 1. **Name**: how the entry appears everywhere it is selected later 2. **Description**: optional context, up to 200 characters 3. **Address**: paste the address into the first address block _Image: Create Address Book Entry dialog with the name filled in and one Solana address added_ ## Address Type Detection Pasting an address sets its type for you. The **Auto-Detect** badge marks the type as read from the address, and the dropdown confirms the match with a **Detected** tag and a validation line below the list. _Image: Address type dropdown listing EVM, SOL, TRON, and BTC, with SOL marked Detected and a Valid SOL address confirmation_ | Address Type | Covers | | --- | --- | | EVM | EVM-compatible networks, including Ethereum, Polygon, and BSC | | SOL | Solana blockchain addresses | | TRON | Tron blockchain addresses | | BTC | Bitcoin blockchain addresses | > **Note:** An address that does not match its selected type returns an error. Select a different type from the dropdown when the address belongs to a chain the detector did not pick. ## Adding Addresses to One Entry Select **Add Address** to append another block. Each block carries its own type and address, and the arrows on a block move it up or down the list. _Image: Create Address Book Entry dialog with a Solana address in the first block and an EVM address in the second, and the Add Address button highlighted_ The saved entry then shows one row with a chain icon per address. _Image: Address Book list showing a single entry holding 2 addresses with Solana and Ethereum icons_ ## Editing and Deleting an Entry Each row on the Address Book list carries an edit icon and a delete icon. The edit dialog matches the creation dialog and opens with the saved addresses in place. Addresses can be added, reordered, or removed there. ## Where the Address Book Applies | Feature | How it uses saved addresses | | --- | --- | | [Withdrawal](https://docs.fystack.io/product/withdrawal.md) | Select a saved recipient when submitting a withdrawal request | | [Automation](https://docs.fystack.io/product/automation.md) | Set a sweep task destination to **Address Book** in place of an internal wallet | | [Policies](https://docs.fystack.io/product/policies.md) | Evaluate rules against the **Whitelisted address** and **Address age** fields | ## Best Practices - Name entries after the counterparty, so one record stays readable as you add addresses to it - Keep every address for one counterparty in a single entry, so a policy rule covers all of them at once - Verify the full address with the counterparty through a separate channel before saving it - Use the description to record who owns the address and who approved adding it - Review entries when a counterparty rotates addresses, and delete records you no longer send to --- # Automation Fystack's automation tools help businesses streamline their crypto operations, reduce development time, and minimize operational overhead. Instead of building complex fund management systems from scratch, teams can focus on their core business while ensuring secure and efficient treasury operations. ### Key Benefits - **Development Efficiency**: Implement automated fund movements without writing complex blockchain monitoring code - **Operational Excellence**: Reduce manual treasury operations and minimize human error - **Team Management**: Simplify fund consolidation across multiple business units and wallets - **Risk Reduction**: Automate the movement of funds to secure storage based on configurable thresholds _Image: Fystack Auto Consolidation_ ## Sweep Tasks A sweep task automatically transfers funds from source wallets to a destination wallet based on configured triggers. ### Common Use Cases - **Exchange Operations**: Automatically sweep user deposits to hot wallet, cold storage - **Payment Gateway**: Process merchant settlements to operational wallets - **Treasury**: Consolidate funds from multiple business wallets - **Risk Management**: Move funds from hot wallets to secure storage Read more about [automation use cases and best practices](https://fystack.io/blog/from-hot-to-cold-smart-secure-cryto-fund-movement-with-fystack) in our detailed guide. ## Creating Sweep Tasks ### During Wallet Creation (API, SDKs) You can set up automatic sweeping when creating a new wallet through our API or SDKs: ```typescript // Create wallet with new sweep task await sdk.createWallet({ name: 'User Payment Wallet', walletType: WalletType.Standard, sweepTaskParams: { minTriggerValueUsd: '100.00', destinationWalletId: '123e4567-e89b-12d3-a456-426614174001', destinationType: 'internal_wallet' } }) ``` > **Note:** You can also attach an existing sweep task to reuse the same configuration across multiple wallets: > > ```typescript > await sdk.createWallet({ > name: 'User Payment Wallet', > walletType: WalletType.Standard, > sweepTaskId: '987f6543-e21c-43b2-a654-321098765432' // Reuse existing task > }) > ``` > > This simplifies management as updating one sweep task affects all attached wallets. See [Create a Wallet](https://docs.fystack.io/wallets.md#create-a-wallet) for complete wallet creation details. ### Create Sweep Task Manually _Image: Wallet Configuration_ 1. Navigate to Automation > Add Sweep Task _Image: image-2025-09-15T14-37-20-837Z_ 2. Enter task name and select "periodic" strategy _Image: image-2025-09-15T14-37-57-659Z_ 3. Trigger Configuration - **Minimum Trigger Value (USD)**: Balance threshold that triggers a sweep - **Check Frequency (Seconds)**: How often to check source wallets ### Wallet Configuration 1. **Source Wallets** - Select wallets to monitor for sweeping - Multiple wallets can be selected _Image: image-2025-09-15T14-39-01-087Z_ 2. **Destination Wallet** Choose either: - **Internal Wallet**: Select from workspace wallets - **Address Book**: Use saved wallet addresses _Image: image-2025-09-15T14-39-22-626Z_ > **Note:** Source wallets are automatically excluded from destination options to prevent circular sweeping. ## Updating Sweep Tasks You can modify existing sweep tasks: 1. **Change Destination Wallet** - Click on the task you want to update - Click on the destination wallet dropdown - Choose a new destination wallet _Image: image-2025-09-15T14-40-22-977Z_ 2. **Adjust Trigger Settings** - Modify minimum trigger value - Update check frequency 3. **Task Status** - Toggle the task status switch to activate/deactivate the sweep task - Deactivating a task temporarily stops all automatic sweeping - Reactivate anytime to resume sweeping with the same configuration _Image: image-2025-09-15T14-42-27-909Z_ ## Deleting a Sweep Task > **Note:** Before deleting a sweep task, ensure that: > > - All attached wallets have alternative sweep configurations if needed > - Any ongoing sweeps are completed To delete a sweep task: 1. Navigate to the sweep tasks list view 2. Find the task you want to delete 3. Click the kebab menu (⋮) on the right side of the task 4. Select "Delete" from the menu options _Image: Deleting a sweep task_ > **Note:** Once deleted, the sweep task will be permanently removed and all automatic sweeping for attached wallets will stop immediately. ## Best Practices - Test with small amounts first - Start with higher check frequencies for critical operations - Verify destination addresses before activating - Monitor initial transfers to ensure correct setup > **Note:** Sweep tasks require source wallets to have enough gas to execute on-chain transfers. Use [Gas Station](https://docs.fystack.io/product/gas-station.md) to keep your Gas Tank funded across all networks, and enable the TronZap fee saving integration to significantly reduce gas costs on TRON Mainnet sweeps. --- # Gas Station The **Gas Station** is Fystack's built-in gas management hub. It provides a dedicated **Gas Tank** wallet that holds native tokens across multiple chains, ensuring your wallets always have enough gas to execute transactions without manual top-ups. > **Note:** Gas Station works hand-in-hand with [Automation](https://docs.fystack.io/product/automation.md). When a sweep task consolidates funds across wallets, Gas Station ensures each source wallet always has enough gas to execute those automated transfers, with no manual top-ups required. _Image: Gas Station overview showing the Gas Tank wallet and supported assets_ ## Gas Tank Overview The Gas Tank is a special wallet inside Gas Station that holds gas tokens for each supported network. From the **Overview** tab you can see your balance per asset, deposit gas funds, or withdraw them. | Tab | Description | | --- | --- | | Overview | Total balance and per-asset breakdown across all supported networks (BNB, ETH, POL, SOL, TRX, and more) | | Withdraw | Move gas funds out of the Gas Tank back to any external address | | Transaction | Full history of gas deposits and withdrawals for the Gas Tank wallet | | Fee Saving | Configure energy-renting providers (e.g. TronZap) to reduce transaction fees on supported networks | > **Note:** The Gas Tank must hold native tokens before it can cover transaction fees. Follow the steps below to add assets and deposit funds. --- ## How to Fund the Gas Tank ### Step 1: Add an Asset On the **Overview** tab, click **+ Add Asset** in the top-right corner to open the asset picker. _Image: Gas Station Overview tab with the Add Asset button highlighted_ ### Step 2: Choose from the List or Add a Custom Token The **Add Assets Wallet** modal shows all supported assets (ETH, BTC, SOL, BNB, POL, and more). Select the one you need. If the token you want is not in the list, click **Add non-listed asset →** at the bottom to add a custom token instead. _Image: Add Assets Wallet modal showing the asset list and Add non-listed asset option_ For a **custom token**, select the network and paste the token contract address, then click **Add Custom Token**. _Image: Add Assets Wallet modal for a custom token showing Network and Address fields_ ### Step 3: Deposit Funds Once the asset appears in the Overview list, click **Deposit** on its row. A modal displays the Gas Tank deposit address and a QR code. Send the native token to this address from any external wallet or exchange. _Image: Deposit modal showing the Gas Tank QR code and wallet address_ > **Note:** The deposit address only accepts the corresponding native token. Sending a different token to this address may result in permanent loss of funds. --- ## Fee Saving: Tron Energy Renting with TronZap TRON Mainnet transactions require **Energy**: the resource that powers smart contract execution. Without energy, the network deducts TRX directly from the sender's wallet as a fee. By integrating **TronZap**, Fystack automatically rents the required energy on your behalf before each transaction, reducing fees by up to **60%** with zero network fee charged to the sender. ### How it works When a TRON withdrawal is triggered, Fystack checks if a Fee Saving configuration is active. If so, it calls TronZap to rent the exact amount of energy needed for that transaction. The withdrawal goes through gasless with **Network Fee: 0**, and TronZap deducts only the energy rental cost in TRX from your TronZap balance. ### Step 1: Navigate to Gas Station and Open Fee Saving Click **Gas Station** in the left sidebar, then select the **Fee Saving** tab. You will see the TRON Mainnet Fee Saving section with an empty configuration list. Click **+ Add Config** to start. _Image: Fee Saving tab inside Gas Station with the Add Config button_ ### Step 2: Get Your API Key from TronZap In the **Create Fee Saving Config** modal, click **Get Your API Key** next to the API Key field. This opens the TronZap platform where you will retrieve your credentials. _Image: Create Fee Saving Config modal showing the Get Your API Key link_ On the TronZap dashboard, make sure your account has a TRX balance (minimum 2 TRX to start renting energy). Top up via the **Recharge Address** shown on your dashboard if needed. _Image: TronZap dashboard showing wallet balance and recharge address_ Then go to **API** in the TronZap sidebar to find your **API Token** and **API Secret**. Copy both values, as you will need them in the next step. _Image: TronZap API Credentials page showing API Token and API Secret_ > **Note:** Keep your **API Secret** private. Never share it or commit it to source control. You can reset it from the TronZap API page at any time. ### Step 3: Enter API Credentials in Fystack Back in the **Create Fee Saving Config** modal, fill in the following fields: 1. **Provider**: pre-filled as `TronZap (TRON Mainnet)` 2. **Enabled**: toggle on to activate immediately after saving 3. **API Key**: paste the API Token copied from TronZap 4. **API Secret**: paste the API Secret copied from TronZap 5. **Default Energy Amount**: the amount of energy to rent per transaction (default: `65,000`, which covers a standard USDT transfer on TRON) Click **Create** to save the configuration. _Image: Create Fee Saving Config modal with API Key, API Secret, and Default Energy Amount filled in_ ### Step 4: Verify the Configuration is Active After saving, the **Configurations** list shows TronZap with: - **Status:** Enabled - **API Key:** Configured - **Default Energy:** 65,000 Your TRON fee saving is now live. You can edit or delete the config at any time from this screen. _Image: Fee Saving Configurations list showing TronZap as Enabled and Configured_ ### Fee Saving in Action: Gasless Withdrawals When you initiate a TRON Mainnet withdrawal, Fystack detects the active TronZap config and automatically rents the required energy. The withdrawal modal shows **Network Fee: 0** with a green notice confirming the energy rental and the amount saved. _Image: Withdrawal modal showing Network Fee 0 and TronZap gasless energy rental saving_ ### Monitoring Rent History The **Rent History** sub-tab logs every energy rental attempt with full details: wallet, network, status, energy amount, cost in TRX, order ID, and on-chain transaction hash. Use this to audit usage and troubleshoot any failed rentals. _Image: Rent History table showing successful TronZap energy rental for a Tron Mainnet transaction_ | Column | Description | | --- | --- | | Time | When the energy rental was triggered | | Wallet | The Fystack wallet that initiated the transaction | | Network | The chain on which energy was rented (e.g. Tron Mainnet) | | Status | SUCCESS or FAILED. Failed rentals fall back to standard network fees. | | Energy Amount | Units of energy rented for that transaction | | Cost | TRX spent on energy rental | | Order ID / Tx Hash | TronZap order reference and on-chain transaction hash for verification | ## Best Practices - **Keep your TronZap balance topped up**: if the TRX balance on TronZap runs out, energy rental fails and transactions fall back to standard fees. - **Set Default Energy Amount appropriately**: `65,000` covers a standard USDT TRC-20 transfer. Complex smart contract interactions may require more; adjust based on your transaction types. - **Monitor Rent History regularly**: any FAILED entries mean that transaction paid full network fees. Investigate via the Order ID or Tx Hash. - **Rotate your API Secret periodically**: treat it like a password and regenerate it from TronZap's API page on a regular schedule. --- # Alerts The **Alerts** module lets you monitor wallet activity in real time and receive instant notifications through external channels like Telegram. Set up event-based or condition-based alerts on specific wallets, route them to your team's communication channels, and get notified the moment something happens without watching a dashboard. Alerts are organized into two tabs: - **Alert**: the alerts you have configured and their current status - **Channels**: the notification destinations (e.g. Telegram groups) that receive alert messages --- ## Set Up a Notification Channel A notification channel is the delivery destination for your alerts. Currently, Fystack supports **Telegram** channels and groups. ### Step 1: Navigate to Alerts and Open the Channels Tab Click **Alerts** in the left sidebar, then select the **Channels** tab. Any existing channels appear here. Click **+ Create Channel** to add a new one. _Image: Alerts Channels tab showing existing channels and the Create Channel button_ ### Step 2: Fill in Channel Details In the **Create Notification Channel** modal, complete the following fields: 1. **Channel Type**: select `Telegram` from the dropdown 2. **Channel Name**: a label to identify this channel within Fystack (e.g. `Marketing Department`, `Ops Team`) 3. **Description**: an optional note explaining what this channel is for Click **Create Channel** to save. _Image: Create Notification Channel modal with Channel Type, Channel Name, and Description fields_ ### Step 3: Connect the Channel to Telegram After creation, the channel appears in the list with **Connected: False**. It needs to be linked to a Telegram chat before it can deliver alerts. Click the **Connect channel** icon (the link icon) on the right side of the row. _Image: Channels list showing the newly created Demo Channel with Connected set to False_ _Image: Connect channel icon highlighted on the Channels list row_ ### Step 4: Link the Fystack Alert Bot in Telegram The **Channel Connection Setup** dialog opens with a unique connection code and instructions. To link the channel: 1. Open Telegram and search for **@FystackAlertBot** 2. Start a direct chat with the bot, or add it to the group or channel you want to receive notifications in 3. Send the connection code shown in the dialog: click the code field to copy it, then paste it as a message to the bot _Image: Channel Connection Setup dialog showing the Telegram bot name and connection code to send_ The bot replies with a confirmation: **"Channel successfully linked! You will now receive alerts here."** _Image: Telegram chat with Fystack Alert Bot confirming the channel is successfully linked_ > **Note:** For team notifications, add **@FystackAlertBot** to a Telegram group or channel and make sure it has permission to send messages. For personal notifications, a direct chat with the bot is sufficient. ### Step 5: Click Complete Setup Back in the **Channel Connection Setup** dialog, the status indicator now shows **Connected**. Click **Complete Setup** to finalize the configuration. _Image: Channel Connection Setup dialog with Connected status and the Complete Setup button highlighted_ The Channels list now shows **Connected: True** for this channel. It is ready to receive alert notifications. _Image: Channels list with Demo Channel now showing Connected as True_ --- ## Create an Alert > **Note:** You must have at least one connected channel before creating an alert. If you have not set one up yet, follow the steps above first. Go to the **Alert** tab and click **+ Create Alert** to set up a monitoring rule. _Image: Alert tab showing the existing alerts list and the Create Alert button_ ### Step 1: Choose the Alert Type The **Create Alert** wizard begins by asking what you want to monitor. Select the type that fits your use case, then click **Next**. _Image: Create Alert type selection showing Event Alert and Trigger Alert options_ | Type | When it fires | Example use cases | | --- | --- | --- | | Event Alert | A specific wallet action occurs: deposit received, withdrawal requested, withdrawal approved, withdrawal cancelled, or withdrawal executed | Notify the ops team every time a withdrawal is executed on a hot wallet | | Trigger Alert | A wallet state crosses a defined condition: balance drops below a threshold, transaction volume exceeds a limit, or unusual activity is detected | Alert when USDC balance on a treasury wallet drops below 100 | ### Step 2: Select Event Types and Target Wallets For an **Event Alert**, choose one or more wallet events to monitor from the list: - **Incoming Deposit**: triggered when funds are received - **Withdrawal Request**: when a withdrawal is submitted - **Withdrawal Approval**: when a withdrawal is approved by a [Signer](https://docs.fystack.io/product/user-management.md) - **Withdrawal Cancel**: when a withdrawal is cancelled - **Withdrawal Executed**: when a withdrawal is completed on-chain Then select one or more **Target Wallets** to scope the alert. The summary at the bottom confirms how many event types and wallets will be monitored. _Image: Create Alert step showing event type selection and target wallet picker_ > **Note:** For **Trigger Alerts**, this step instead asks you to define the condition (e.g. `balance gte 100 USDC`) and the wallet to evaluate. The condition is checked continuously against live wallet state. ### Step 3: Select Notification Channels Choose which notification channels should receive messages for this alert. All connected channels appear in the **Add Notification Channel** picker. Select one or more, confirm the summary at the bottom, then click **Create Alert**. _Image: Create Alert step showing the notification channel selector with Demo Channel selected_ ### Alert is Active The new alert appears in the **Alert** list with **Active: True**. Each row shows the alert type, the event or trigger condition, the wallets being monitored, the creator, and the linked channels. _Image: Alert list showing multiple active alerts with their types, events, wallets, and channels_ ### Testing Your Alerts The fastest way to verify an alert is working is to trigger one of the monitored events manually. For a **Withdrawal Request** or **Withdrawal Executed** event alert, initiate a [withdrawal](https://docs.fystack.io/withdrawals.md) from one of the target wallets. _Image: Withdraw modal with a test withdrawal to verify the alert notification_ Once the event fires, the Fystack Alert Bot delivers a structured message to your Telegram channel within seconds. The message includes the event type, wallet name, transaction ID, amount, network, recipient address, and a link to the block explorer. _Image: Telegram message from Fystack Alert Bot showing a Withdrawal Requested notification with full transaction details_ --- ## Best Practices - **Create one channel per team or function**: separate channels for ops, finance, and compliance make it easier to route the right alerts to the right people without noise. - **Use Event Alerts for audit trails**: pairing **Withdrawal Approval** and **Withdrawal Executed** alerts gives your team a real-time record of every transaction, complementing the workspace Audit Log. - **Use Trigger Alerts for balance monitoring**: set a minimum balance threshold on wallets used with [Automation](https://docs.fystack.io/product/automation.md) sweep tasks, so you catch low-balance situations before they block automated transfers. - **Test every new alert before relying on it**: trigger the monitored event manually right after setup to confirm the bot message arrives in the expected Telegram chat. - **Add the bot to a group for team visibility**: individual DMs are easy to miss. A shared Telegram group ensures the whole team sees critical events in real time. --- # Policies Set programmable guardrails that automatically allow, block, or route sensitive actions through normal approval in your workspace. Policies evaluate every withdrawal and smart contract call against rules you define, so risky activity is stopped before it ever reaches the blockchain. ## Overview A **policy** is a set of rules that runs automatically whenever someone initiates a guarded action, such as requesting a withdrawal or calling a smart contract. Each rule describes a condition (for example, "withdrawal value over $10,000 to a non-whitelisted address") and an effect: - **Deny**: block the action outright - **Allow**: let the action skip the approval queue and proceed directly Policies act as an always-on layer of defense that complements your signer approvals. Instead of relying on every approver to remember your treasury rules, you encode those rules once and let Fystack enforce them on every transaction. _Image: Policy List_ ## How Policies Are Evaluated When a guarded action runs, Fystack checks it against every active policy that targets the relevant wallet and applies a custody-safe decision model: 1. **Deny always wins**: if any rule matches with a **Deny** effect, the action is blocked, even if another rule would allow it. 2. **Allow bypasses approval**: if a rule matches with an **Allow** effect and nothing denies, the action skips the approval queue and proceeds. 3. **No match**: if no rule matches, the action continues through your normal approval flow. > **Note:** Policy evaluation is **fail-closed**. If a policy cannot be evaluated for any > reason, the action is denied rather than allowed through. This guarantees a > misconfiguration or system issue can never silently weaken your controls. ## Trigger Actions Each rule applies to one trigger action, the type of operation it guards: | Trigger | Guards | | --- | --- | | Withdrawal | Evaluated when a withdrawal is requested. Rules can reference the amount, USD value, asset, destination address, whitelist status, address age, and recent wallet, workspace, and network activity. | | Contract Call | Evaluated when a smart contract method is called. Rules can reference the network, contract address, method selector, method name, native value, gas limit, and decoded call arguments. | | Web3 Transfer | Evaluated when a native-coin or ERC-20 transfer is sent through the Web3 raw-transaction API. Rules use the same withdrawal fields (amount, USD value, asset, destination, address book entry, internal wallet), so one condition reads the same on both triggers. ERC-20 transfers are also evaluated as Contract Call. | | Web3 Message Signing | Evaluated when a wallet is asked to sign a message (`personal_sign`, `eth_sign`, `eth_signTypedData_v4`). Rules can detect token permits (EIP-2612, DAI, Permit2), their spender, tokens, amount, and deadline. Deny only: a signed message always needs approval. | ## How to Create a Policy ### Step 1: Open the Policies Page In the left sidebar, open **Policies**, then click **Create Policy**. ### Step 2: Name the Policy and Choose an Approval Group Give the policy a clear **Name** that describes what it protects (for example, "Large withdrawal guard"). Then select the [**Approval Group**](https://docs.fystack.io/product/approval-group.md) that will review changes to this policy. Every policy is tied to an approval group. This means a policy cannot be silently created or edited, any change must be reviewed and approved, the same way a withdrawal is. _Image: Create Policy Form_ > **Note:** The approval group is preselected with your workspace's default. You can > change it to any group whose members should be responsible for reviewing this > policy's rules. ### Step 3: Target Specific Wallets (Optional) By default, a policy applies to **all wallets** in the workspace. To scope it to a subset, select one or more wallets in the **Wallets** field. The policy's rules will only be evaluated for actions on those wallets. _Image: Wallet Selector_ ### Step 4: Add Rules Add one or more rules to the policy. For each rule, choose the **Trigger** (Withdrawal, Contract Call, Web3 Transfer, or Web3 Message Signing), build the **Condition**, and pick the **Effect** (Allow or Deny). The condition builder shows the fields available for the selected trigger, so you can construct expressions without memorizing field names. To skip writing rules from scratch, start from one of the [Policy Templates](#policy-templates) below. _Image: Add Rule Form_ Policies also programmable with JSON builder, which is useful for power users who want to copy/paste rules or use more complex logic than the UI supports. _Image: Policy JSON Builder_ ### Step 5: Submit for Approval Click **Create**. Because every policy is approval-gated, the new policy is not active yet, it is submitted to its approval group as a **Pending Approval** request, along with all of its rules. _Image: Policy Approval Request_ ### Step 6: Approve the Policy A member of the assigned approval group opens the pending request, reviews the proposed policy and its rules, and approves it. Only then does the policy become **Active** and start guarding transactions. _Image: Approve Policy Modal_ > **Note:** The same approval flow applies to later policy and rule changes: editing a > rule, adding a rule, or deleting the policy creates a new approval request. > Nothing takes effect until the group signs off. ## Policy Templates Start from a template instead of a blank policy. Each one below is a complete policy for a common wallet setup: what it protects, the rules in plain English, and JSON you can paste straight into the policy editor. > **Note:** The numbers in each template are starting points, not recommendations for > your volume. A good default is to set each Deny cap at about 2x to 3x your > busiest normal day, then adjust after a week of real traffic. ### Using a Template 1. Go to **Policies**, click **Create Policy**, and switch the editor to the **JSON** tab. 2. Paste the template's **Policy JSON**. 3. Switch back to **Form**. Pick the **Approval Group** and the **Wallets** the policy should cover. The templates leave both empty on purpose, and an empty wallet list means every wallet in the workspace. 4. Open each rule and replace any placeholder in angle brackets, such as ``. Rules that refer to a member, API key, wallet, or address book entry show a picker in the builder, so you can select it by name instead of looking up its ID. 5. Adjust the limits to your own volume, then click **Create** and get the policy approved. You can also send the same JSON to the [Policies API](https://docs.fystack.io/policies.md#create-a-policy). ### Choose a Template | Template | Apply to | Auto-approves | Main limits | | --- | --- | --- | --- | | Treasury vault | Custody and treasury wallets | Nothing | Address book only, 24h cooling-off, 250,000 USD per day per wallet | | Hot wallet payouts | Operational wallets used by your team | Small payouts to trusted addresses | 2,000 USD to unknown addresses, 50,000 USD per day, 60 per hour | | API payout bot | Wallets an automated API key pays out from | Payouts up to 500 USD from one key | Stablecoins only, 2,000 USD per payout, 20,000 USD per day for the key | | Deposit sweep | Customer deposit wallets | Sweeps to your treasury wallet | Funds can only move to the treasury wallet | | DeFi operations | Wallets that use Web3 and dApps | Nothing | Approved contracts only, no unknown tokens, no risky signatures | | Workspace backstop | All wallets | Nothing | 500,000 USD per day across the workspace, weekend freeze on large withdrawals | > **Note:** Policies stack. Apply the **Workspace backstop** to all wallets, then add one > template per wallet group. Because Deny always wins, the backstop also caps > anything a narrower policy auto-approves. ### Treasury Vault **Use it for**: wallets that hold reserves and move funds a few times a week. Nothing is auto-approved, so every transfer still goes to your approvers, and the rules block the transfers approvers should never be asked to sign. **Wallets**: select your custody and treasury wallets. | Effect | Rule | Why | | --- | --- | --- | | **Deny** | Block withdrawals to any external address that is not in the address book. | A typo or a swapped clipboard address can never be approved by mistake. | | **Deny** | Block withdrawals to an address added to the address book less than 24 hours ago. | Someone who can edit the address book cannot add an address and drain funds to it on the same day. | | **Deny** | Block a withdrawal that would push the wallet over 250,000 USD in 24 hours, counting pending withdrawals. | Limits the damage if approvers are compromised or tricked. | | **Deny** | Apply the address book rule to native and ERC-20 transfers sent through the Web3 API. | Closes the path around the withdrawal rules. | ```json { "name": "Treasury vault guard", "description": "Hard limits for custody and treasury wallets", "approval_group_id": "", "wallet_ids": [], "rules": [ { "trigger_action": "withdrawal.create", "condition": "!withdrawal.destination_is_internal && !withdrawal.is_whitelisted", "effect": "DENY", "description": "Only send to address book entries or workspace wallets" }, { "trigger_action": "withdrawal.create", "condition": "withdrawal.is_whitelisted && withdrawal.address_age_sec < 86400", "effect": "DENY", "description": "New address book addresses wait 24 hours" }, { "trigger_action": "withdrawal.create", "condition": "wallet.projected_outflow_24h > 250000", "effect": "DENY", "description": "Cap each wallet at 250000 USD per 24 hours" }, { "trigger_action": "web3.transfer", "condition": "!withdrawal.destination_is_internal && !withdrawal.is_whitelisted", "effect": "DENY", "description": "Apply the address book rule to Web3 transfers" } ] } ``` **Tune it** - Need a stricter cooling-off period? Change `86400` (24 hours) to `172800` (48 hours). - To keep custody funds inside the workspace, so they can only move to your own hot wallets, add a Deny rule: `wallet.purpose == 'custody' && !withdrawal.destination_is_internal`. - To cap a single asset in its own units, add `withdrawal.asset_symbol == 'BTC' && wallet.asset_projected_outflow_24h > 5`. ### Hot Wallet Payouts **Use it for**: operational wallets your team pays out from every day. Small, routine payouts to known recipients skip approval, and everything else is either reviewed or blocked. **Wallets**: select your hot or payment wallets. | Effect | Rule | Why | | --- | --- | --- | | **Deny** | Block withdrawals over 2,000 USD to addresses that are not in the address book. | Larger amounts must go to a vetted address. | | **Deny** | Block a withdrawal that would push the wallet over 50,000 USD in 24 hours. | Caps the worst case, including the auto-approved payouts. | | **Deny** | Block withdrawals once the wallet has sent 60 in the last hour. | Catches a script or a compromised session that sends many small withdrawals. | | **Allow** | Skip approval for withdrawals up to 1,000 USD to an address that has been in the address book for at least 24 hours. | Approvers stop spending time on routine payouts. | **What happens to a withdrawal** | Withdrawal | Result | | --- | --- | | 500 USD to an address book entry added last week | Auto-approved by the Allow rule | | 500 USD to an address added an hour ago | Goes to your approvers, because the address is too new for the Allow rule | | 1,500 USD to an unknown address | Goes to your approvers | | 5,000 USD to an unknown address | Blocked | | 800 USD to a trusted address when the wallet has already sent 49,500 USD today | Blocked, because Deny always wins over Allow | ```json { "name": "Hot wallet payouts", "description": "Auto approve routine payouts and cap the worst case", "approval_group_id": "", "wallet_ids": [], "rules": [ { "trigger_action": "withdrawal.create", "condition": "!withdrawal.destination_is_internal && !withdrawal.is_whitelisted && withdrawal.value_usd > 2000", "effect": "DENY", "description": "Unknown recipients are limited to 2000 USD" }, { "trigger_action": "withdrawal.create", "condition": "wallet.projected_outflow_24h > 50000", "effect": "DENY", "description": "Cap each wallet at 50000 USD per 24 hours" }, { "trigger_action": "withdrawal.create", "condition": "wallet.withdrawal_count_1h > 60", "effect": "DENY", "description": "Stop bursts of more than 60 withdrawals per hour" }, { "trigger_action": "withdrawal.create", "condition": "withdrawal.is_whitelisted && withdrawal.address_age_sec >= 86400 && withdrawal.value_usd <= 1000", "effect": "ALLOW", "description": "Auto approve small payouts to trusted addresses" } ] } ``` **Tune it** - Raise or lower the auto-approve limit (`1000`) first. Keep it well below the per-wallet daily cap. - To auto-approve only some recipients, such as your exchange accounts, replace `withdrawal.is_whitelisted` in the Allow rule with **Destination · Address book entry** is any of the entries you trust. - If your team works set hours, add the business-hours recipe from Rule Recipes below. ### API Payout Bot **Use it for**: a backend service that sends customer payouts through an API key. Small payouts go out without waiting for approvers, medium ones are reviewed, and the key has a hard daily budget. **Wallets**: select the wallets the payout service sends from. | Effect | Rule | Why | | --- | --- | --- | | **Deny** | Block the payout key from sending anything except USDT and USDC. | A leaked key cannot move your other assets. | | **Deny** | Block any single payout from the key over 2,000 USD. | Larger payouts must be created by a person. | | **Deny** | Block the key once its payouts would exceed 20,000 USD in 24 hours, across all wallets. | A daily budget for the key, whichever wallet it uses. | | **Allow** | Skip approval for payouts up to 500 USD from the key. | Customers get small payouts right away. Payouts from 500 to 2,000 USD go to your approvers. | ```json { "name": "API payout bot", "description": "Budget and auto approval for the payout API key", "approval_group_id": "", "wallet_ids": [], "rules": [ { "trigger_action": "withdrawal.create", "condition": "principal.api_key_id in [''] && !(withdrawal.asset_symbol in ['USDT', 'USDC'])", "effect": "DENY", "description": "Payout key sends stablecoins only" }, { "trigger_action": "withdrawal.create", "condition": "principal.api_key_id in [''] && withdrawal.value_usd > 2000", "effect": "DENY", "description": "Payout key cannot send more than 2000 USD at once" }, { "trigger_action": "withdrawal.create", "condition": "principal.api_key_id in [''] && workspace.initiator_projected_outflow_24h > 20000", "effect": "DENY", "description": "Payout key is capped at 20000 USD per 24 hours" }, { "trigger_action": "withdrawal.create", "condition": "principal.api_key_id in [''] && withdrawal.value_usd <= 500", "effect": "ALLOW", "description": "Auto approve payouts up to 500 USD" } ] } ``` **Tune it** - Replace `` in every rule, or pick the key with the **Initiator · API key** picker. Use the same key in all four rules. - Customer payout addresses are usually not in your address book, so this template does not require it. The per-payout and daily caps carry the risk instead. - Add more symbols to the list, such as `'DAI'`, if you pay out in other stablecoins. ### Deposit Sweep **Use it for**: per-customer deposit wallets whose funds should only ever move to your treasury. The destination is fixed, so this is the safest flow to fully automate. **Wallets**: select your customer deposit wallets, or leave the list empty to rely on the wallet purpose check. | Effect | Rule | Why | | --- | --- | --- | | **Deny** | Block withdrawals from deposit wallets to anywhere except your treasury wallet. | Deposits can never leave the workspace directly. | | **Allow** | Skip approval for transfers from deposit wallets to your treasury wallet. | Sweeps run without waiting for anyone. | ```json { "name": "Deposit sweep", "description": "Deposit wallets sweep to treasury only", "approval_group_id": "", "wallet_ids": [], "rules": [ { "trigger_action": "withdrawal.create", "condition": "wallet.purpose == 'user' && !(withdrawal.destination_wallet_id in [''])", "effect": "DENY", "description": "Deposit wallets can only send to the treasury" }, { "trigger_action": "withdrawal.create", "condition": "wallet.purpose == 'user' && withdrawal.destination_wallet_id in ['']", "effect": "ALLOW", "description": "Auto approve sweeps to the treasury" } ] } ``` **Tune it** - Replace ``, or pick it with the **Destination · Workspace wallet** picker. - To allow more than one sweep target, add their IDs to both lists. ### DeFi Operations **Use it for**: wallets that interact with dApps through the Web3 API. Every call still goes to your approvers, and the rules stop calls and signatures approvers often cannot judge from the raw data. **Wallets**: select the wallets you connect to dApps. | Effect | Rule | Why | | --- | --- | --- | | **Deny** | Block calls to any contract that is not on your list. | A phishing dApp cannot get a call to an unknown contract in front of approvers. | | **Deny** | Block token approvals (`approve`, selector `0x095ea7b3`) on any contract except your token list. | Allowances are a common way to drain a wallet later. | | **Deny** | Block transfers of tokens that are not in the Fystack asset catalog. | Unknown tokens have no USD value, so amount limits cannot apply to them. | | **Deny** | Block signing a token permit with an unlimited amount. | A signed permit lets the spender move tokens later, with no further transaction from you. | | **Deny** | Block signing a permit for any spender that is not on your list. | Permit phishing usually asks you to sign for an attacker spender. | | **Deny** | Block `eth_sign` requests. | `eth_sign` can sign a hidden transaction, and modern dApps do not need it. | ```json { "name": "DeFi operations guard", "description": "Approved contracts and safe signatures for dApp wallets", "approval_group_id": "", "wallet_ids": [], "rules": [ { "trigger_action": "web3.contract_call", "condition": "!(resource.contract_address in ['', '', ''])", "effect": "DENY", "description": "Only call approved contracts" }, { "trigger_action": "web3.contract_call", "condition": "resource.method_selector == '0x095ea7b3' && !(resource.contract_address in [''])", "effect": "DENY", "description": "Only approve allowances on known tokens" }, { "trigger_action": "web3.transfer", "condition": "!withdrawal.asset_is_known", "effect": "DENY", "description": "Block transfers of unknown tokens" }, { "trigger_action": "web3.sign_message", "condition": "resource.is_permit && resource.permit_amount_is_unlimited", "effect": "DENY", "description": "Block unlimited token permits" }, { "trigger_action": "web3.sign_message", "condition": "resource.is_permit && !(resource.permit_spender in [''])", "effect": "DENY", "description": "Block permits to untrusted spenders" }, { "trigger_action": "web3.sign_message", "condition": "resource.method == 'eth_sign'", "effect": "DENY", "description": "Block raw eth_sign" } ] } ``` **Tune it** - Contract and token addresses must be lowercase. - Add every contract your strategy uses, such as routers, vaults, and token contracts, before you enable the policy, or legitimate calls will be blocked. - Message signing rules are Deny only. A signature that passes these rules still goes to your approvers. ### Workspace Backstop **Use it for**: a safety net across every wallet. It catches a drain spread across many wallets, keys, or members, which a single wallet limit would miss. **Wallets**: leave the list empty so the policy covers all wallets. | Effect | Rule | Why | | --- | --- | --- | | **Deny** | Block a withdrawal that would push the whole workspace over 500,000 USD in 24 hours. | The absolute ceiling, whatever other policies allow. | | **Deny** | Block withdrawals once the workspace has sent 300 in the last hour. | Catches automated abuse spread across wallets. | | **Deny** | Block withdrawals over 25,000 USD on Saturday and Sunday (UTC). | Large transfers wait until your team is around to watch them. | ```json { "name": "Workspace backstop", "description": "Workspace wide ceilings for all wallets", "approval_group_id": "", "wallet_ids": [], "rules": [ { "trigger_action": "withdrawal.create", "condition": "workspace.projected_outflow_24h > 500000", "effect": "DENY", "description": "Cap the workspace at 500000 USD per 24 hours" }, { "trigger_action": "withdrawal.create", "condition": "workspace.withdrawal_count_1h > 300", "effect": "DENY", "description": "Stop bursts of more than 300 withdrawals per hour" }, { "trigger_action": "withdrawal.create", "condition": "context.day_of_week in ['Saturday', 'Sunday'] && withdrawal.value_usd > 25000", "effect": "DENY", "description": "No withdrawals over 25000 USD on weekends" } ] } ``` **Tune it** - Days and hours are in UTC. - Set the workspace cap above the sum of your normal busiest days across all wallets, so the backstop only fires on real anomalies. ### Rule Recipes Single rules you can add to any policy. Paste the condition into the **Expression** tab of a rule, or build it in **Fields**. | Goal | Trigger | Effect | Condition | | --- | --- | --- | --- | | Block an address list, such as sanctioned or scam addresses | Withdrawal | Deny | `` `withdrawal.address_book_id in ['']` ``. Keep the addresses in one address book entry, so updating the entry updates the rule. | | Only owners and admins can send over 50,000 USD | Withdrawal | Deny | `` `!(principal.role in ['owner', 'admin']) && withdrawal.value_usd > 50000` `` | | Only allow large withdrawals during business hours (09:00 to 18:00 UTC) | Withdrawal | Deny | `` `withdrawal.value_usd > 25000 && (context.hour < 9 \|\| context.hour >= 18)` ``. Uses brackets, so write it in the Expression tab. | | Cap a wallet at 5 BTC per day | Withdrawal | Deny | `` `withdrawal.asset_symbol == 'BTC' && wallet.asset_projected_outflow_24h > 5` `` | | Cap one network at 100,000 USD per day | Withdrawal | Deny | `` `withdrawal.network_code == '' && workspace.network_projected_outflow_24h > 100000` `` | | Auto-approve transfers between your own wallets | Withdrawal | Allow | `` `withdrawal.destination_is_internal && withdrawal.value_usd <= 10000` `` | | Auto-approve top-ups to trusted exchange accounts | Withdrawal | Allow | `` `withdrawal.address_book_id in [''] && withdrawal.address_age_sec >= 172800 && withdrawal.value_usd <= 25000` `` | | Stop work piling up in the approval queue | Withdrawal | Deny | `` `wallet.pending_outflow > 100000` ``. Blocks new requests while 100,000 USD is already waiting. | | Block unusually expensive contract calls | Contract Call | Deny | `` `resource.gas_limit > 1000000` `` | ### Rolling Out a Policy Allow rules change who has to look at a transaction, so introduce them last. 1. **Week 1, Deny rules only**: create the policy with only its Deny rules. Nothing is auto-approved yet, and the rules only block what should never happen. 2. **Review the approval queue**: look for withdrawals your approvers approve unchanged every time, such as the same recipients, assets, and amount range. 3. **Add a narrow Allow rule**: write an Allow rule for exactly that traffic, with a lower limit than you expect to need. 4. **Raise limits slowly**: widen the Allow rule only after it has run cleanly for a while. Each change goes through approval and appears in the policy's [Change History](#change-history). > **Note:** When you add an Allow rule, check that a Deny cap covers the same wallets, > for example a per-wallet or per-key projected outflow limit. Deny always > wins, so the cap still applies to auto-approved withdrawals. ## Writing Rule Conditions A condition is an expression that evaluates to true or false. When it is true, the rule's effect applies. Combine fields with `and`, `or`, and `not`, and compare values with operators like `>`, `>=`, `==`, and `!=`. _Image: Condition Builder_ > **Note:** You can validate a condition before saving it. The builder checks the > expression against the trigger's available fields and flags any errors, so you > never submit a rule that cannot run. ### Condition Fields The fields available in the builder depend on the rule's trigger. Use the labels below when deciding which field belongs in a rule. #### Available on Every Trigger | Field | Use it for | | --- | --- | | Initiator type | Whether the action came from a web session (`user`) or an API key (`api_key`). | | Member | Rules for specific workspace members. Pick them from the member list. | | API key | Rules for specific API keys, for example an automated payout key. Web sessions have no API key, so combine a `is none of` check with Initiator type. | | User role | Rules based on workspace role: owner, admin, signer, proposer, or viewer. | | Hour | Time-of-day controls using UTC hour from 0 to 23. | | Day of week | Business-hour, weekend, or weekday-only rules. | | Source wallet | Rules for specific wallets inside one policy. | | Source wallet purpose | Rules by wallet purpose: general, custody, payment, user, one-time use, gas tank, or deployment. | | Source wallet type | Different controls for MPC and standard wallets. | | User email (legacy) | Still evaluated for existing rules. New rules use Member, because an email can change. | #### Withdrawal Fields | Field | Use it for | | --- | --- | | Amount | Asset-denominated limits, such as more than 10 ETH or 1 BTC. | | Value USD | Dollar-denominated limits across assets. | | Asset symbol | Asset-specific rules for symbols such as USDT, ETH, BTC, or SOL. | | Native asset | Different controls for native network assets versus tokens. | | Destination address | Blocking or allowing specific recipient addresses. | | Network code | Network-specific limits, such as stricter rules on one chain. | | In address book | Controls for recipients that are or are not in any address book entry. | | Address book entry | Controls for specific address book entries. One entry covers all of its addresses, so an entry works as a named list. | | Address age | Cooling-off rules: seconds since the destination address was added to its address book entry. | | Internal wallet | Controls for transfers between wallets of the same workspace. | | Workspace wallet | Controls for transfers to specific wallets of the same workspace. | > **Note:** Fields that refer to a member, an API key, an address book entry, or a > wallet use a picker in the condition builder. Fystack stores the ID, shows the > name everywhere the rule appears (including approval requests), and checks > when you save that each ID belongs to your workspace. If an entity is later > deleted or revoked, the rule shows it as an unknown reference and the rule > can only match less, never more. #### Withdrawal Activity Fields | Field | Use it for | | --- | --- | | Wallet outflow | Per-wallet USD outflow over the last 1, 6, or 24 hours. | | Wallet withdrawal count | Burst controls for many withdrawals from one wallet in the last hour. | | Workspace outflow | Workspace-wide USD outflow over the last 1, 6, or 24 hours. | | Workspace withdrawal count | Workspace-wide withdrawal volume in the last hour. | | Network outflow | Workspace USD outflow on the selected network over the last 1, 6, or 24 hours. | | Network withdrawal count | Workspace withdrawal volume on the selected network in the last hour. | #### Pending and Projected Activity Fields | Field | Use it for | | --- | --- | | Pending outflow | USD value of withdrawals that are not executed yet (waiting for approval, queued, or funding gas), whatever their age. Available per wallet, workspace, and network. | | Projected 24h outflow | Realized outflow in the last 24 hours, plus pending outflow, plus the withdrawal being checked. Use it for limits: `projected 24h outflow over $100,000` means this withdrawal would exceed the limit. Available per wallet, workspace, and network. | | Projected 24h asset outflow | The same projection for the source wallet, in units of the withdrawn asset, for example at most 5 BTC per day. | | Initiator projected 24h outflow | The same projection for withdrawals created by the same member or API key, across the workspace. | > **Note:** Outflow and count fields are calculated from realized withdrawals: they count > funds after a withdrawal has been executed or confirmed. Pending and projected > fields also count withdrawals that are not executed yet. When an active rule > uses a pending or projected field, Fystack checks the workspace's withdrawals > one at a time, so two requests sent at the same moment cannot both pass a > limit. A request that waits longer than 5 seconds for its turn is rejected > with HTTP 429 and can be retried. #### Contract Call Fields | Field | Use it for | | --- | --- | | Network code | Chain-specific contract call rules. | | Contract address | Blocking unknown contracts or allowing known contracts. | | Method selector | Low-level function checks, such as ERC-20 transfer selector `0xa9059cbb`. | | Method name | Human-readable method checks when metadata is available. | | Value wei | Native value sent with a contract call. | | Gas limit | Blocking unusually expensive or risky calls. | | Decoded arguments | Argument-level rules when Fystack can decode the call. ERC-20 transfers expose recipient and token value. | > **Note:** Decoded arguments are only available when the call can be decoded. In the > JSON builder, guard decoded argument checks before reading a value. #### Web3 Transfer Fields Web3 Transfer rules use the [Withdrawal Fields](#withdrawal-fields) plus the fields below. Activity fields are not available, because these transfers are not withdrawals. | Field | Use it for | | --- | --- | | Asset in catalog | False when the token contract is not in the asset catalog. Amount, USD value, and symbol are then empty, so deny unknown tokens explicitly if you need to. | | Token contract | Rules for a specific token contract. Empty for native coins. | #### Message Signing Fields | Field | Use it for | | --- | --- | | Signing method | `personal_sign`, `eth_sign`, or `eth_signTypedData_v4`. | | Typed data type, domain name, verifying contract | Rules for specific typed-data messages, such as a known dApp's orders. | | Is a token permit | True for EIP-2612, DAI, and Permit2 messages, which let the spender move tokens later without another signature. | | Permit spender | Deny permits to spenders you do not trust. | | Permit tokens | The token contracts the permit covers. Use it in the expression editor, for example `'0x…' in resource.permit_tokens`. | | Permit unlimited amount | True for max-value allowances (2159 or more, or a DAI permit with allowed set). | | Permit deadline | Unix time the permit or its signature expires. 0 when the message has none. | ## Policy Statuses | Status | Meaning | | --- | --- | | Pending | The policy, or a change to it, is awaiting sign-off from its approval group. It is not yet enforced. | | Active | The policy is approved and live. Its rules are evaluated against every guarded action on the targeted wallets. | | Inactive | The policy is disabled and not evaluated. | | Rejected | A pending change was rejected by an approver and did not take effect. | ## Change History Every change to a policy is recorded. The policy's **History** shows each revision with who proposed it, what changed (field by field), and whether it was approved or rejected. This gives you a complete audit trail of how your guardrails evolved over time. _Image: Policy Change History_ ## Best Practices - Start with **Deny** rules for your highest-risk scenarios (large transfers, unknown destinations) before adding convenience **Allow** rules - Remember that **Deny always wins**, use this to set hard limits that no Allow rule can override - Scope policies to specific wallets when different wallets have different risk profiles, rather than applying one broad policy everywhere - Use **Allow** rules sparingly, they bypass your approval queue, so reserve them for low-value, clearly trusted patterns - When an **Allow** rule trusts address book entries, keep the address age condition the builder adds. Anyone who can edit the address book can add an address to a trusted entry, and the age check makes a new address wait before it is auto-approved - Validate every condition before submitting, and review the change in the approval step to confirm it behaves as intended - Treat policy changes with the same care as withdrawals, the approval gate exists so no single person can weaken your controls unnoticed --- # Two-Factor Authentication Fystack supports two methods of two-factor authentication (2FA) to protect your account: **Passkey** (biometrics or security keys) and **TOTP** (time-based one-time passwords via an authenticator app). Both can be configured independently or together. You can also set your preferred method, the one Fystack prompts first during login. Passkeys are the recommended approach for most users. They rely on your device's built-in authentication (fingerprint, Face ID, or PIN) and are resistant to phishing since they are bound to the exact domain of the application. ## Navigating to Security Settings Click your account avatar in the top-right corner of the dashboard and select **Security** from the dropdown menu. _Image: Click the user avatar and select Security from the dropdown_ This opens the **User Settings** page, where you can manage your Two-Factor Authentication methods. _Image: User Settings page showing Two-Factor Authentication options_ The page has two independent sections: - **Passkey Authentication**: passwordless authentication using biometrics or security keys - **TOTP Authentication**: time-based codes from an authenticator app (Google Authenticator, Authy, etc.) ## Setting Up Passkey Authentication ### Step 1: Open the Register dialog In the **Passkey Authentication** section, click **+ Add Passkey**. A dialog appears asking you to name your passkey — use something descriptive like "My Laptop" or "iPhone 15" to identify the device later. _Image: Register A Passkey dialog with a name field_ Click **Register Passkey** to proceed. ### Step 2: Choose where to save your passkey Your browser or operating system will prompt you to choose where to store the passkey. Options typically include your password manager (e.g., Google Password Manager) or a physical security key or nearby phone. _Image: Prompt to choose where to save the passkey_ ### Step 3: Create the passkey After selecting a storage location, the browser will ask you to confirm the creation of a passkey for `app.fystack.io`. Click **Create** to proceed. _Image: Browser prompt to create a passkey for app.fystack.io_ ### Step 4: Verify with your device A **Verification Required** prompt will appear. Click **Use Passkey** and authenticate using your device's method (fingerprint, Face ID, or PIN). _Image: Verification Required prompt to use your passkey_ If you are on a desktop and want to authenticate with your phone or tablet instead, you can scan the QR code that appears after clicking **Use Passkey**. _Image: QR code option to sign in with a passkey from a phone or tablet_ ### Step 5: Passkey setup complete Once authentication succeeds, your passkey appears under the Passkey Authentication section as the **Primary authentication method**, with the device name and registration date. _Image: User Settings showing the registered passkey as primary authentication method_ > **Info.** You can register multiple passkeys — one per device. This is useful if you need to authenticate from both a laptop and a phone. ## Setting Up TOTP Authentication TOTP requires an authenticator app such as Google Authenticator, Authy, or 1Password. It generates a fresh 6-digit code every 30 seconds and works as a second layer on top of your password or passkey. ### Step 1: Open the TOTP configuration Scroll down to the **TOTP Authentication** section and click **Configure TOTP**. _Image: User Settings with the Configure TOTP button highlighted_ ### Step 2: Scan the QR code A dialog titled **Turn On 2-Step Verification** displays a QR code. Open your authenticator app and scan this QR code to link it to your Fystack account. If your app does not support scanning, click **Or enter the code manually** to copy the secret key. _Image: Turn On 2-Step Verification dialog with QR code_ Click **Continue** after scanning. ### Step 3: Enter the verification code Your authenticator app will display a 6-digit code. Enter it into the **Verify Authentication Code** dialog and click **Complete 2-Step Verification**. _Image: Verify Authentication Code dialog with 6-digit code input_ ### Step 4: Confirm with your passkey To protect against unauthorized changes, Fystack requires a passkey verification before enabling TOTP. A **Verification Required** prompt appears. Click **Use Passkey** and authenticate with your device. _Image: Passkey Verification Required before TOTP is enabled_ ### Step 5: TOTP setup complete A success notification confirms that TOTP authentication is now active. The **TOTP Authentication** section shows a green "TOTP is configured" indicator, and the Two-Factor Authentication banner at the top reflects both active methods. _Image: User Settings showing both Passkey and TOTP configured successfully_ > **Success.** **Recommendation**: configure both Passkey and TOTP for the highest level of account protection. If one method becomes unavailable, you can fall back to the other. ## Best Practices - Register a passkey on each device you use regularly. This avoids QR-code cross-device prompts on your primary machine and keeps authentication fast. - Use TOTP as a backup method, not a replacement for Passkey. If you lose access to your device, TOTP from a separate phone provides a reliable recovery path. - Periodically review your registered passkeys under User Settings and remove any that belong to devices you no longer use or own. --- # Workspace A **Workspace** is the top-level organizational unit in Fystack. It acts as an isolated environment where you manage wallets, users, transactions, and settings independently from other workspaces. This makes it ideal for separating clients, business units, or environments (e.g. production vs. staging) under a single Fystack account. _Image: Fystack Demo Workspace dashboard overview_ ## Key Concepts - **Isolation** — Each workspace has its own wallets, user roles, API keys, and transaction history. Data does not bleed between workspaces. - **Multi-workspace** — A single Fystack account can belong to multiple workspaces with different roles in each (e.g. Owner in one, Signer in another). - **Default workspace** — One workspace can be marked as the default, so it is automatically selected when you log in. - **Role scoping** — User roles (Owner, Admin, Signer, Proposer, Viewer, Guest) are enforced per workspace. A user's role in workspace A has no bearing on workspace B. ## How to Create a Workspace ### Step 1 — Sign In to Fystack Go to the Fystack login page and authenticate using your email and password or via **Continue with Google**. _Image: Fystack sign-in page_ ### Step 2 — Select a Workspace or Create a New One After a successful login, Fystack shows the **Workspace Selector** — a list of all workspaces your account belongs to, along with your role in each. Click a workspace to enter it. _Image: Join Your Workspace screen listing existing workspaces and the Create Workspace button_ > **Note:** In this example, the **company** workspace is selected (role: Signer). If this is your first time using Fystack or you want a fresh environment, click **Create Workspace** directly from this screen to skip straight to Step 4. ### Step 3 — Navigate the Workspace and Find the Workspace Switcher Once inside a workspace, you can see all the workspaces your account belongs to directly from the sidebar. Click the **workspace name** at the top-left to expand the list. Here you can jump between workspaces or scroll to the bottom to find **Create New Workspace**. _Image: Sidebar showing the active workspace, other workspaces, and the Create New Workspace button_ > **Note:** This is how most users create additional workspaces — from within an existing one, without ever logging out. ### Step 4 — Fill in the Workspace Details Click **Create New Workspace** (sidebar) or **Create Workspace** (login selector) to open the creation modal. Provide: 1. **Name** — a descriptive label for the workspace (e.g. `Fystack Demo Workspace`, `Ops_Fund`, `Marketing Team`). 2. **Set as default workspace** (optional) — toggle this on if you want Fystack to land you in this workspace automatically on every subsequent login. Click **Create Workspace** to confirm. _Image: Create Workspace modal with Name field and Set as default workspace toggle_ > **Note:** Choose a name that clearly reflects the team or purpose. Workspace names must be unique within your account. ### Step 5 — Your New Workspace is Ready Fystack immediately switches you into the new workspace. The workspace name appears in the top-left of the sidebar. Because it is brand new, the wallet list starts empty — click **+ Create Wallet** to start managing assets. _Image: Newly created Fystack Demo Workspace with an empty wallet dashboard_ ## Workspace Dashboard Overview After entering a workspace, the main dashboard gives you an at-a-glance summary: | Section | Description | | --- | --- | | Total Value | Aggregate USD value of all assets across all wallets in the workspace | | Total Wallets | Number of wallets created inside the workspace | | Latest Transactions | Real-time feed of the 10 most recent on-chain transactions across all wallets | | Wallet List | Filterable and sortable table of all wallets with type, top assets, and current value | ## Best Practices - **One workspace per client or environment** — Keep production and staging assets fully separated to avoid accidental cross-environment transactions. - **Set a default workspace** — If you work primarily in one workspace, mark it as default to reduce friction on every login. - **Name workspaces clearly** — Prefer names like `Acme_Production` or `Ops_Fund` over generic names like `Workspace 1`, especially when managing multiple clients. - **Review workspace membership regularly** — Use [User Management](https://docs.fystack.io/product/user-management.md) to audit who has access to each workspace and remove stale members promptly. --- # User Management Manage workspace members, control access levels, and maintain operational security through Fystack's User Management module. Add collaborators, assign roles appropriate to their responsibilities, and update permissions as your team evolves. _Image: User Management overview showing the member list_ ## Role Overview Each user in a Fystack workspace is assigned a role that defines their permissions. Roles are enforced at both the API and UI level to ensure least-privilege access across all operations. | Role | Description | Key Capabilities | | --- | --- | --- | | Owner | Workspace creator with full, unrestricted access | Manage billing, assign all roles, delete workspace | | Admin | Full control over the Workspace and all wallets within it | Create/manage wallets, invite users, configure settings | | Signer | Can review and approve or cancel transactions | Approve or reject pending transactions, view all wallets | | Proposer | Can create and propose transactions but cannot approve them | Initiate transactions, view wallet balances | | Viewer | Read-only access to the Workspace and wallets | View transactions, balances, and reports | | Guest | Limited, temporary access to specific wallets or functionalities | Scoped read or action access as configured | > **Note:** Only one **Owner** exists per workspace. The Owner role is automatically assigned to the account that created the workspace and cannot be transferred through the invite flow. ## How to Invite a User ### Step 1: Open User Management After signing in, click **User Management** in the left sidebar. You will see a list of all current workspace members with their email, role, and status. _Image: Navigate to User Management from the sidebar_ ### Step 2: Click "Invite User" Click the **Invite User** button in the top-right corner of the User Management page. _Image: Invite User button in the top-right corner_ ### Step 3: Fill in Invitation Details In the **Invite User** modal, complete the following fields: 1. **Email**: the invitee's email address (e.g. `ted@fystack.io`) 2. **Username**: a display name for the user within the workspace 3. **Select Role**: choose the access level appropriate for this user: - **Admin**: Full control over the Workspace and all wallets within it - **Signer**: Can review and approve or cancel transactions - **Proposer**: Can create and propose transactions but cannot approve them - **Viewer**: Read-only access to the Workspace and wallets - **Guest**: Limited, temporary access to specific wallets or functionalities Click **Send Invitation** to dispatch the email. _Image: Invite User modal showing email, username, and role selection_ > **Note:** Select the role that matches the user's operational responsibility. For transaction workflows requiring maker-checker controls, pair a **Proposer** (creates transactions) with a **Signer** (approves them). ### Step 4: Invitee Accepts the Email The invitee receives an email from `noreply@fystack.io` with the subject **"You have been assigned a role"**. They must click **Accept Invitation** to activate their workspace access. _Image: Invitation email with the Accept Invitation button_ ### Step 5: User Becomes Active Once the invitation is accepted, the new user appears in the User Management list with **Active** status and their assigned role. They can now log in and access the workspace according to their role permissions. _Image: New user listed as Active with Signer role after accepting the invitation_ ## Updating a User's Role or Status To change a member's role or deactivate their access after they have joined: 1. Locate the user in the **User Management** list 2. Click the **Update** button (pencil icon) on the right side of their row 3. Select a new role or toggle their status 4. Confirm the change > **Note:** Deactivating a user immediately revokes their login access without removing their workspace history. All past transactions and associations they created remain intact for audit purposes. ## Best Practices - Assign the **least-privilege** role that allows the user to perform their tasks - Use **Proposer** and **Signer** together to enforce maker-checker approval on transactions - Group your Signers into an [**Approval Group**](https://docs.fystack.io/product/approval-group.md) and assign it to each wallet, so withdrawals need several signatures rather than one - Regularly review the member list and deactivate accounts that are no longer needed - Reserve **Admin** for users who genuinely need to manage wallets or workspace settings. Use **Viewer** for read-only stakeholders. --- # Asset Management The **Asset** page lists every cryptocurrency asset configured for your workspace, along with its contract address, decimal precision, and current status. It lives under the **Developers** section of the sidebar and serves as the reference point for which tokens your wallets can send and receive. _Image: Asset Management page showing the list of configured assets with status, address, and decimal columns_ Each row in the asset list contains the following information: | Column | Description | | --- | --- | | Asset | Token name and description (e.g. USDC — Binance-Peg USD Coin) | | Status | Active means the asset is enabled and available for transactions | | Address | The on-chain contract address. Native tokens (e.g. ETH, TRX) show "Native Token" instead of a contract address. | | Decimals | The token's decimal precision, used to correctly display and calculate amounts | | Created At / Updated At | Timestamps for when the asset was added and last modified | --- ## Supported Networks Fystack currently supports assets on the following networks: | Network | Native Token | | --- | --- | | Ethereum | ETH | | Base | ETH | | Polygon | POL | | Solana | SOL | | BNB Chain | BNB | | TRON | TRX | | Lisk | LSK | | Bitcoin | BTC | --- ## Filtering Assets Click **Filters** to narrow the asset list by two properties: - **Whitelisted**: filter by whether the asset is on your workspace whitelist - **Native Asset**: filter to show only native chain tokens (e.g. ETH, TRX) or only contract-based tokens _Image: Asset Filters panel showing Whitelisted and Native Asset dropdowns_ You can also use the **All Networks** dropdown in the top-right corner to filter by a specific chain. --- ## Requesting a New Asset If the token you need is not in the list, click **+ Create Asset** to submit a request. _Image: Asset list with the Create Asset button highlighted_ Fill in the **Create Asset** form with the following details: 1. **Name**: the full token name (e.g. `USD Coin`) 2. **Symbol**: the ticker symbol (e.g. `USDC`) 3. **Address**: the on-chain contract address of the token 4. **Decimals**: the token's decimal precision (check the token's official documentation or block explorer) 5. **Logo URL**: a publicly accessible URL to the token's logo image 6. **Network**: the chain the token lives on 7. **Whitelist**: toggle on to add the token to your workspace whitelist 8. **Native Token**: toggle on only if this is a chain's native gas token Click **Submit** to send the request. _Image: Create Asset form showing Name, Address, Symbol, Decimals, Logo URL, Network, Whitelist, and Native Token fields_ > **Note:** Asset creation is not self-service. After you submit the form, the Fystack team reviews and activates the asset on your behalf. Contact Fystack support if you need an asset added urgently. --- # Network Management Manage and monitor your blockchain networks efficiently. Configure network settings, monitor health status, and maintain data integrity through our network management tools. _Image: image-2025-09-15T14-14-49-920Z_ ## Creating a Network _Image: image-2025-09-15T14-15-41-838Z_ To add a new blockchain network: 1. Click the "Create Network" button in the top right corner 2. Fill in the network details: - **Name**: Network identifier - **ChainId**: Unique blockchain identifier - **Description**: Network details and purpose - **Native Currency**: Network's native token - **Explorer Token**: Block explorer API token - **Explorer TX/Address**: Block explorer URLs - **Internal Code**: Internal reference code - **Confirmation Blocks**: Number of blocks required for transaction confirmation - **Block Interval**: Time between blocks in seconds - **Logo URL**: Network logo image URL - **Network Type**: - Testnet: Test network configuration - EVM: Ethereum Virtual Machine compatibility - Disabled: Temporarily disable network > **Note:** Confirmation blocks are crucial for transaction security. Higher numbers mean better security but slower confirmation times. ## Network Health Status _Image: image-2025-09-15T14-16-29-401Z_ Networks display one of three health statuses based on RPC response times: - **Healthy** (🟢): Response time < 10ms - **Slow** (🟡): Response time between 10-50ms - **Degraded** (🔴): Response time > 50ms The system automatically monitors RPC performance and updates status accordingly. ## Network Rescan The Missing Block Rescan feature ensures blockchain data integrity by allowing manual re-indexing of specific block ranges. _Image: image-2025-09-15T14-17-03-295Z_ ### When to Use Rescan Use the rescan feature when: - RPC outages occur - Network downtime is experienced - Indexer synchronization issues arise - Missing transactions need recovery ### Running a Rescan 1. Click the "Rescan" button for the target network 2. Select the network from the dropdown - Note: Only operational networks with active RPC connections are available _Image: image-2025-09-15T14-18-01-876Z_ 3. Specify the block range: - **From Block**: Starting block number - **To Block**: Ending block number 4. Click "Start Rescan" to begin the process > **Note:** The rescan process ensures no deposits, transfers, or critical events are lost in the database by re-indexing the specified block range. ## Best Practices - Regularly monitor network health status - Configure appropriate confirmation blocks based on security needs - Use rescan feature proactively after any network issues - Keep explorer tokens and URLs updated - Document any network-specific configurations --- # API Documentation Use the Fystack API to create and manage secure MPC wallets, authorize transactions and integrate advanced wallet features like transaction policies and multi-approval flows, all without ever touching a private key. Our API is designed for developers who want enterprise-grade security with plug-and-play simplicity. Get started in minutes using our RESTful endpoints or one of our fully featured SDKs. [Quickstart](https://docs.fystack.io/quickstart.md) [Explore SDKs](https://docs.fystack.io/sdks.md) [Build with AI](https://docs.fystack.io/building-with-ai.md) ## Guides - [Authentication](https://docs.fystack.io/authentication.md): Learn how to authenticate your API requests. - [Pagination](https://docs.fystack.io/pagination.md): Understand how to work with paginated responses. - [Errors](https://docs.fystack.io/errors.md): Read about the different types of errors returned by the API. - [Webhooks](https://docs.fystack.io/webhooks.md): Learn how to programmatically configure webhooks for your app. ## APIs - [Workspace](https://docs.fystack.io/product/workspace.md): Isolated environments for managing wallets, users, transactions, and settings. - [Network Management](https://docs.fystack.io/product/network.md): Configure network settings and monitor network health. - [Asset Management](https://docs.fystack.io/product/asset-management.md): Every asset configured for your workspace, with its contract address and status. - [User Management](https://docs.fystack.io/product/user-management.md): Add members, assign roles, and update permissions. - [Approval Group](https://docs.fystack.io/product/approval-group.md): Choose who signs off on sensitive actions and how many signatures are required. - [Security & 2FA](https://docs.fystack.io/product/security.md): Secure your account with Passkey and TOTP two-factor authentication. - [Withdrawal](https://docs.fystack.io/product/withdrawal.md): Send funds with address risk screening, category tagging, and approval queues. - [Address Book](https://docs.fystack.io/product/address-book.md): Save counterparty addresses across chains for withdrawals, sweeps, and policies. - [Web3 Sign](https://docs.fystack.io/product/web3-sign.md): Review and approve Web3 signing requests in the dashboard. - [Gas Station](https://docs.fystack.io/product/gas-station.md): Keep wallets funded with gas from a shared multi-chain Gas Tank. - [Automation & Sweep](https://docs.fystack.io/product/automation.md): Sweep funds between wallets automatically with sweep tasks. - [Policies](https://docs.fystack.io/product/policies.md): Automatically allow, block, or route sensitive actions through approval. - [Alerts](https://docs.fystack.io/product/alerts.md): Get real-time wallet activity alerts in channels like Telegram. - [API Keys](https://docs.fystack.io/api-keys.md): Get the HMAC secret or generate an Ed25519 key pair to sign API requests. - [Wallets](https://docs.fystack.io/wallets.md): Create and manage secure wallets for digital assets with standard and MPC options. - [Transfers](https://docs.fystack.io/withdrawals.md): Initiate and manage transfers from your wallets. - [Web3 Sign](https://docs.fystack.io/web3-sign.md): Sign messages, typed data, and transactions with Fystack wallets. - [Webhooks](https://docs.fystack.io/webhooks.md): Receive events from Fystack and verify webhook payloads. - [Pay](https://docs.fystack.io/checkout.md): Create checkouts and accept crypto payments from your users. - [Networks](https://docs.fystack.io/networks.md): Retrieve supported blockchain networks and rescan transactions. - [Assets](https://docs.fystack.io/assets.md): Query supported assets and add or remove them from wallets. - [Workspaces](https://docs.fystack.io/workspaces.md): Retrieve workspace statistics, balances, and analytics. - [Policies](https://docs.fystack.io/policies.md): Create, validate, and manage programmable policy rules. ## APIs - [Workspace](https://docs.fystack.io/product/workspace.md): Isolated environments for managing wallets, users, transactions, and settings. - [Network Management](https://docs.fystack.io/product/network.md): Configure network settings and monitor network health. - [Asset Management](https://docs.fystack.io/product/asset-management.md): Every asset configured for your workspace, with its contract address and status. - [User Management](https://docs.fystack.io/product/user-management.md): Add members, assign roles, and update permissions. - [Approval Group](https://docs.fystack.io/product/approval-group.md): Choose who signs off on sensitive actions and how many signatures are required. - [Security & 2FA](https://docs.fystack.io/product/security.md): Secure your account with Passkey and TOTP two-factor authentication. - [Withdrawal](https://docs.fystack.io/product/withdrawal.md): Send funds with address risk screening, category tagging, and approval queues. - [Address Book](https://docs.fystack.io/product/address-book.md): Save counterparty addresses across chains for withdrawals, sweeps, and policies. - [Web3 Sign](https://docs.fystack.io/product/web3-sign.md): Review and approve Web3 signing requests in the dashboard. - [Gas Station](https://docs.fystack.io/product/gas-station.md): Keep wallets funded with gas from a shared multi-chain Gas Tank. - [Automation & Sweep](https://docs.fystack.io/product/automation.md): Sweep funds between wallets automatically with sweep tasks. - [Policies](https://docs.fystack.io/product/policies.md): Automatically allow, block, or route sensitive actions through approval. - [Alerts](https://docs.fystack.io/product/alerts.md): Get real-time wallet activity alerts in channels like Telegram. - [API Keys](https://docs.fystack.io/api-keys.md): Get the HMAC secret or generate an Ed25519 key pair to sign API requests. - [Wallets](https://docs.fystack.io/wallets.md): Create and manage secure wallets for digital assets with standard and MPC options. - [Transfers](https://docs.fystack.io/withdrawals.md): Initiate and manage transfers from your wallets. - [Web3 Sign](https://docs.fystack.io/web3-sign.md): Sign messages, typed data, and transactions with Fystack wallets. - [Webhooks](https://docs.fystack.io/webhooks.md): Receive events from Fystack and verify webhook payloads. - [Pay](https://docs.fystack.io/checkout.md): Create checkouts and accept crypto payments from your users. - [Networks](https://docs.fystack.io/networks.md): Retrieve supported blockchain networks and rescan transactions. - [Assets](https://docs.fystack.io/assets.md): Query supported assets and add or remove them from wallets. - [Workspaces](https://docs.fystack.io/workspaces.md): Retrieve workspace statistics, balances, and analytics. - [Policies](https://docs.fystack.io/policies.md): Create, validate, and manage programmable policy rules. --- # Quickstart This guide will get you up and running with the Fystack API - your all-in-one platform for secure MPC wallet creation, and transaction management. By the end, you’ll know exactly how to start building secure, scalable crypto applications with Fystack. > **Note:** Before you can make requests to the Fystack API, you will need to grab your > API key from your dashboard. You find it under [API Keys](https://app.fystack.io/api-key). 👉 To begin, follow these quick steps: ## 1. Login into your workspace _Image: Fystack dashboard showing API key setup_ ## 2. Get your API Key Use your API key to make **secure, authenticated calls** to Fystack’s API.\ Your key is scoped by workspace and supports **IP whitelisting** and **policy enforcement**. _Image: create api key image_ _Image: get api key image_ ### 3. Install the SDK (Optional) Use our official SDKs for **TypeScript**, **JavaScript**, or **Go** for seamless integration: ```bash npm install @fystack/sdk ``` ```bash go get github.com/fystack/platform-sdk-go ``` ## 4. Create Your First Wallet Use the API or SDK to create your first **MPC wallet** with just a few lines of code —\ **no private key management required**. **SDK** ```typescript import { FystackSDK, Environment } from "@fystack/sdk" const apiCredentials = { apiKey: process.env.API_KEY, apiSecret: process.env.API_SECRET } const sdk = new FystackSDK({ credentials: apiCredentials, environment: Environment.Sandbox, logger: true }) const { wallet_id } = await sdk.createWallet({ name: 'MPC wallet 1', walletType: WalletType.MPC }) ``` ## 5. Make first deposit and monitor **SDK** ```typescript const response = await sdk.getDepositAddress(wallet_id, AddressType.Evm) console.log(response) ``` ``` { "address": "0xb71e841C003735F58f7A264c92DcDE6E6b9d6e4e", "qr_code": "https://quickchart.io/qr?size=300&margin=2&text=0xb71e841C003735F58f7A264c92DcDE6E6b9d6e4e" } ``` ## What's next? Great, you're now set up with an API client and have made your first request to the API. Here are a few links that might be handy as you venture further into the Fystack API: - [Learn about the different error messages in Fystack](https://docs.fystack.io/errors.md) --- # Fystack SDKs The easiest way to integrate with Fystack is via our official SDKs for TypeScript and Go. Both sign every request for you (HMAC, Ed25519 or AWS KMS), and cover wallets, deposit addresses, withdrawals, Web3 signing, sweeps, checkout, and webhook verification. Install one, add your API key, and you’re ready to build. ## Install TypeScript and JavaScript, from [npm](https://www.npmjs.com/package/@fystack/sdk): ```bash npm install @fystack/sdk ``` Go, from [GitHub](https://github.com/fystack/platform-sdk-go): ```bash go get github.com/fystack/platform-sdk-go ``` ```go import fystack "github.com/fystack/platform-sdk-go" client, err := fystack.NewClient(fystack.ClientOptions{ Credentials: fystack.Credentials{ APIKey: os.Getenv("FYSTACK_API_KEY"), APISecret: os.Getenv("FYSTACK_API_SECRET"), }, WorkspaceID: os.Getenv("FYSTACK_WORKSPACE_ID"), Environment: fystack.EnvironmentProduction, }) ``` See the [Go SDK README](https://github.com/fystack/platform-sdk-go#readme) for Ed25519 and AWS KMS signers, withdrawals, EVM and Solana signing, and payments. ## Official SDKs - [Typescript](https://www.npmjs.com/package/@fystack/sdk): Available on NPM - [Go](https://github.com/fystack/platform-sdk-go): Available on GitHub --- # Building with AI Fystack documentation is written for people and for the AI assistants they work with. Every page is available as clean Markdown, and the whole site is indexed in an `llms.txt` file, so tools like Claude Code, Cursor, Codex and ChatGPT can read the docs directly instead of scraping HTML. ## Plain text docs AI tools work best with plain text. Markdown uses far fewer tokens than rendered HTML and keeps code samples, parameter lists and tables intact. Fystack publishes the docs in these forms: - **`llms.txt`**: [https://docs.fystack.io/llms.txt](https://docs.fystack.io/llms.txt) is a short index of every page with a one-line description. Point your assistant here first so it can find the right page on its own. - **`llms-full.txt`**: [https://docs.fystack.io/llms-full.txt](https://docs.fystack.io/llms-full.txt) contains every page in a single file. Use it when you want the assistant to load all of the docs at once and your tool has room in its context window. - **Markdown pages**: add `.md` to the end of any docs URL to get that page as Markdown, for example [https://docs.fystack.io/wallets.md](https://docs.fystack.io/wallets.md). - **Content negotiation**: agents that send an `Accept: text/markdown` header get Markdown from the normal page URL, with no `.md` needed. ```bash curl -H "Accept: text/markdown" https://docs.fystack.io/wallets ``` ## Copy a page into your assistant Every page has a **Copy page** button next to its title. Use it to: - **Copy page**: copy the current page as Markdown, ready to paste into any chat or coding assistant. - **View as Markdown**: open the Markdown version of the page in a new tab. - **Open in ChatGPT** or **Open in Claude**: start a new conversation that already points at the page, so you can ask questions about it straight away. ## Add Fystack to your project instructions Most coding assistants read a project instructions file on every task, such as `CLAUDE.md` for Claude Code, `AGENTS.md` for Codex and other agents, or `.cursor/rules` for Cursor. Add this block to it so the assistant always knows where the Fystack docs live and how to use them: ```markdown ## Fystack - Fystack docs index: https://docs.fystack.io/llms.txt Fetch it before writing or changing any code that calls Fystack. - Any Fystack docs page is available as Markdown by adding `.md` to its URL, for example https://docs.fystack.io/wallets.md - Use the official SDK, which signs requests for you: `@fystack/sdk` for TypeScript, or `github.com/fystack/platform-sdk-go` for Go. - Use the sandbox environment while developing. Never hard-code API keys or secrets; read them from environment variables. ``` ## Example prompts These prompts work well in Claude Code, Cursor, Codex or any assistant that can fetch web pages: ```text Read https://docs.fystack.io/llms.txt, then write a Node.js script that uses @fystack/sdk to create an MPC wallet in the sandbox environment and print its EVM deposit address. Read the API keys from environment variables. ``` ```text Using https://docs.fystack.io/webhooks.md, add an Express endpoint that receives Fystack webhooks, verifies the Ed25519 signature and handles deposit events idempotently. ``` ```text Read https://docs.fystack.io/policies.md and draft a policy that requires approval for any withdrawal above 10,000 USD from our treasury wallet. Explain each rule before I apply it. ``` ## Keep your keys and funds safe > **Warning: Treat AI assistants like any other team member.** Give AI assistants the minimum access they need. Use sandbox API keys while building, keep secrets in environment variables rather than in prompts or committed files, and restrict production keys with IP whitelisting. Protect production wallets with [policies](https://docs.fystack.io/policies.md) and [approval groups](https://docs.fystack.io/product/approval-group.md), so no withdrawal can be sent without human approval, even if an assistant makes a mistake. --- # Authentication You'll need to authenticate your requests to access any of the endpoints in the Fystack API. In this guide, we'll look at how authentication works. Fystack uses HMAC-based authentication to securely verify the authenticity of your API requests. ## HMAC Authentication Fystack API uses a secure HMAC-based authentication system that requires you to sign your requests with your API secret. Each request needs three authentication headers: ```bash ACCESS-API-KEY: your_api_key ACCESS-TIMESTAMP: current_unix_timestamp ACCESS-SIGN: base64_encoded_hmac_signature ``` The HMAC signature is generated by combining the HTTP method, request path, timestamp, and request body (if applicable), then signing it with your API secret. ## Example Request with Authentication Headers Here's an example of how to make an authenticated request to the Fystack API: ```bash curl https://api.fystack.io/api/v1/wallets \ -H "ACCESS-API-KEY: your_api_key" \ -H "ACCESS-TIMESTAMP: 1667836889" \ -H "ACCESS-SIGN: YourBase64EncodedSignature==" ``` ## Computing the ACCESS-SIGN To compute the HMAC signature, you'll need to: 1. Create a canonical string from request parameters in fixed order: `method`, `path`, `timestamp`, `body` 2. Sign this string with your API secret using HMAC-SHA256 3. Hex-encode the HMAC digest, then base64-encode the hex string Always keep your API secret safe and reset it if you suspect it has been compromised. Here's how to compute the signature in each language: ```typescript import CryptoJS from 'crypto-js' function computeSignature( apiSecret: string, method: string, path: string, timestamp: string, body: string = '' ): string { // Build canonical string in exact order const canonical = `method=${method}&path=${path}×tamp=${timestamp}&body=${body}` // HMAC-SHA256 -> hex -> base64 const hexDigest = CryptoJS.HmacSHA256(canonical, apiSecret) .toString(CryptoJS.enc.Hex) return btoa(hexDigest) } // Example usage const timestamp = Math.floor(Date.now() / 1000).toString() const signature = computeSignature( 'your_api_secret', 'GET', '/api/v1/workspaces/your_workspace_id/wallets', timestamp ) const headers = { 'ACCESS-API-KEY': 'your_api_key', 'ACCESS-TIMESTAMP': timestamp, 'ACCESS-SIGN': signature } ``` ```go import ( "crypto/hmac" "crypto/sha256" "encoding/base64" "encoding/hex" "fmt" "strconv" "time" ) func computeSignature(apiSecret string, method, path, timestamp, body string) string { // Build canonical string in fixed order message := fmt.Sprintf("method=%s&path=%s×tamp=%s&body=%s", method, path, timestamp, body) // Create HMAC-SHA256 h := hmac.New(sha256.New, []byte(apiSecret)) h.Write([]byte(message)) hexSig := hex.EncodeToString(h.Sum(nil)) // Base64-encode the hex string return base64.StdEncoding.EncodeToString([]byte(hexSig)) } // Usage ts := strconv.FormatInt(time.Now().Unix(), 10) signature := computeSignature( apiSecret, "GET", "/api/v1/workspaces/your_workspace_id/wallets", ts, "", ) ``` ```php str: # Build canonical string in fixed order message = f"method={method}&path={path}×tamp={timestamp}&body={body}" # Create HMAC-SHA256 hex digest hex_sig = hmac.new( api_secret.encode(), message.encode(), hashlib.sha256 ).hexdigest() # Base64-encode the hex string return base64.b64encode(hex_sig.encode()).decode() # Usage timestamp = str(int(time.time())) signature = compute_signature( api_secret, 'GET', '/api/v1/workspaces/your_workspace_id/wallets', timestamp, '' ) headers = { 'ACCESS-API-KEY': api_key, 'ACCESS-TIMESTAMP': timestamp, 'ACCESS-SIGN': signature } ``` ```java import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import java.nio.charset.StandardCharsets; import java.time.Instant; import java.util.Base64; public static String computeSignature(String apiSecret, String method, String path, String timestamp, String body) throws Exception { // Build canonical string in fixed order String canonical = String.format("method=%s&path=%s×tamp=%s&body=%s", method, path, timestamp, body); // Create HMAC-SHA256 Mac mac = Mac.getInstance("HmacSHA256"); mac.init(new SecretKeySpec(apiSecret.getBytes(StandardCharsets.UTF_8), "HmacSHA256")); byte[] digest = mac.doFinal(canonical.getBytes(StandardCharsets.UTF_8)); // Hex-encode the digest StringBuilder hexDigest = new StringBuilder(); for (byte b : digest) { hexDigest.append(String.format("%02x", b)); } // Base64-encode the hex string return Base64.getEncoder().encodeToString(hexDigest.toString().getBytes(StandardCharsets.UTF_8)); } // Usage String timestamp = String.valueOf(Instant.now().getEpochSecond()); String signature = computeSignature( apiSecret, "GET", "/api/v1/workspaces/your_workspace_id/wallets", timestamp, "" ); Map headers = Map.of( "ACCESS-API-KEY", apiKey, "ACCESS-TIMESTAMP", timestamp, "ACCESS-SIGN", signature ); ``` ### Request signing process 1. Build the canonical string in fixed order: `method={METHOD}&path={PATH}×tamp={TIMESTAMP}&body={BODY}` - `method`: HTTP method in uppercase (GET, POST, etc.) - `path`: Request path (e.g. `/api/v1/workspaces/{id}/wallets`) - `timestamp`: UNIX timestamp in seconds - `body`: JSON-stringified request body, or empty string for GET requests 2. Create the HMAC-SHA256 signature: - Sign the canonical string with your API secret - Hex-encode the digest - Base64-encode the hex string 3. Add required headers: - `ACCESS-API-KEY`: Your API key - `ACCESS-TIMESTAMP`: The timestamp used in the signature - `ACCESS-SIGN`: The base64-encoded signature > **Warning.** Keep your API secret secure and never share it. The API key can be shared but the secret should remain private. ## Building the Body String `body` in the canonical string must be the **exact bytes you transmit**, for both HMAC and Ed25519. For a `GET` request, or any request with no body, that's an empty string, never `"{}"` or `null`. The mistake to avoid: serializing the payload twice, once to sign, once when your HTTP client sends it. If what you signed doesn't byte-for-byte match what the server received, `ValidateAccessSign` fails, even though the payload is logically the same JSON. Serialize once, then reuse that exact string both to build the canonical string and as the literal request body, don't hand your HTTP client the original object/dict/map and trust a second, independent serialization call to reproduce it exactly. Depending on the language this either fails loudly (some HTTP clients reject or mishandle a non-string body outright) or fails silently as a hard-to-debug signature mismatch (if the two serialization calls happen to diverge, from a library upgrade, a different default option, or unusual values like `NaN`), so don't rely on either outcome, just don't serialize twice. ```javascript const payload = { name: 'my-integration' } const body = JSON.stringify(payload) // serialize once // sign() uses `body` in the canonical string, exactly as below const signature = computeSignature(apiSecret, 'POST', path, timestamp, body) await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', /* ...auth headers */ }, // `fetch` requires body to already be a string (or Blob/ArrayBuffer/etc), // it will not JSON-serialize `payload` for you, pass `body`, not `payload` body }) ``` ```go payload := map[string]string{"name": "my-integration"} bodyBytes, _ := json.Marshal(payload) // serialize once body := string(bodyBytes) signature := computeSignature(apiSecret, "POST", path, timestamp, body) req, _ := http.NewRequest("POST", url, bytes.NewReader(bodyBytes)) // same bytes ``` ```python import json import requests payload = {'name': 'my-integration'} body = json.dumps(payload) # serialize once signature = compute_signature(api_secret, 'POST', path, timestamp, body) # Pass data=body (the exact string you signed). Passing json=payload # instead would have requests() serialize the dict itself, a second, # independent call that isn't guaranteed to match what you signed requests.post(url, data=body, headers={'Content-Type': 'application/json', **auth_headers}) ``` ```php 'my-integration']; $body = json_encode($payload); // serialize once $signature = computeSignature($apiSecret, 'POST', $path, $timestamp, $body); curl_setopt($ch, CURLOPT_POSTFIELDS, $body); // same string ``` ```java String body = objectMapper.writeValueAsString(payload); // serialize once String signature = computeSignature(apiSecret, "POST", path, timestamp, body); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(url)) .POST(HttpRequest.BodyPublishers.ofString(body)) // same string .build(); ``` ## Ed25519 Signing If your API key uses a [client key pair](https://docs.fystack.io/api-keys.md#client-key-pair) instead of an HMAC secret, sign the same canonical string with your Ed25519 private key instead of computing an HMAC. Two differences from the HMAC flow: - Sign the canonical string bytes directly, there's no separate hashing step, Ed25519 does that internally. - Base64-encode the raw 64-byte signature straight away, there's no hex step. ``` ACCESS-SIGN = base64( Ed25519_Sign(privateKey, canonical_string) ) ``` If you're using `@fystack/sdk`, pass a `signer` (`LocalPrivateKeySigner` or `AwsKmsSigner`) on `credentials` and it does this for you, see [API Keys](https://docs.fystack.io/api-keys.md#client-key-pair). The examples below are for signing without the SDK, in other languages. ```javascript const crypto = require('crypto'); function computeSignature(privateKeyPem, method, path, timestamp, body = '') { const canonical = `method=${method}&path=${path}×tamp=${timestamp}&body=${body}`; const privateKey = crypto.createPrivateKey(privateKeyPem); // No digest algorithm, Ed25519 signs the message directly const signature = crypto.sign(null, Buffer.from(canonical), privateKey); return signature.toString('base64'); } // Usage const timestamp = Math.floor(Date.now() / 1000).toString(); const signature = computeSignature( process.env.PRIVATE_KEY_PEM, 'GET', '/api/v1/workspaces/your_workspace_id/wallets', timestamp ); ``` ```go import ( "crypto/ed25519" "crypto/x509" "encoding/base64" "encoding/pem" "errors" "fmt" ) func computeSignature(privateKeyPEM []byte, method, path, timestamp, body string) (string, error) { canonical := fmt.Sprintf("method=%s&path=%s×tamp=%s&body=%s", method, path, timestamp, body) block, _ := pem.Decode(privateKeyPEM) if block == nil { return "", errors.New("invalid PEM private key") } key, err := x509.ParsePKCS8PrivateKey(block.Bytes) if err != nil { return "", err } edKey, ok := key.(ed25519.PrivateKey) if !ok { return "", errors.New("not an Ed25519 private key") } // ed25519.Sign hashes internally, sign the canonical string directly signature := ed25519.Sign(edKey, []byte(canonical)) return base64.StdEncoding.EncodeToString(signature), nil } ``` ```python import time import base64 from cryptography.hazmat.primitives.serialization import load_pem_private_key def compute_signature(private_key_pem: bytes, method: str, path: str, timestamp: str, body: str = '') -> str: # Build canonical string in fixed order canonical = f"method={method}&path={path}×tamp={timestamp}&body={body}" private_key = load_pem_private_key(private_key_pem, password=None) # .sign() on an Ed25519PrivateKey signs the message directly, no digest argument signature = private_key.sign(canonical.encode()) return base64.b64encode(signature).decode() # Usage timestamp = str(int(time.time())) signature = compute_signature( private_key_pem, 'GET', '/api/v1/workspaces/your_workspace_id/wallets', timestamp ) ``` ```php **Warning.** Never send your Ed25519 private key to Fystack or embed it in client-side/browser code, only the public key is registered. For production, keep the key in a hardware-backed store like AWS KMS instead of a PEM file, see [Client Key Pair](https://docs.fystack.io/api-keys.md#client-key-pair). ### Signing via AWS KMS (non-SDK) If you're using `@fystack/sdk` on Node.js, `AwsKmsSigner` already does this for you, see [Client Key Pair](https://docs.fystack.io/api-keys.md#client-key-pair). For every other backend, or if you just want to see the mechanism, the same approach works with any language's AWS SDK: call KMS `Sign` with `MessageType: RAW` and `SigningAlgorithm: ED25519_SHA_512` over the raw canonical string, then base64-encode the returned signature bytes. This is exactly what `AwsKmsSigner` does internally, the Node.js example below is that same logic written out directly. > **Warning.** `MessageType` must be `RAW`, not `DIGEST`. KMS's `ED25519_SHA_512` algorithm expects the unhashed message and hashes it internally, matching how Ed25519 works everywhere else on this page. Passing `DIGEST` signs a different, non-interoperable value. ```javascript import { KMSClient, SignCommand } from '@aws-sdk/client-kms' async function computeSignature(kmsClient, keyId, method, path, timestamp, body = '') { const canonical = `method=${method}&path=${path}×tamp=${timestamp}&body=${body}` const response = await kmsClient.send(new SignCommand({ KeyId: keyId, Message: Buffer.from(canonical, 'utf8'), MessageType: 'RAW', SigningAlgorithm: 'ED25519_SHA_512' })) return Buffer.from(response.Signature).toString('base64') } ``` ```go import ( "context" "encoding/base64" "fmt" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/kms" "github.com/aws/aws-sdk-go-v2/service/kms/types" ) func computeSignature(ctx context.Context, client *kms.Client, keyId, method, path, timestamp, body string) (string, error) { canonical := fmt.Sprintf("method=%s&path=%s×tamp=%s&body=%s", method, path, timestamp, body) out, err := client.Sign(ctx, &kms.SignInput{ KeyId: aws.String(keyId), Message: []byte(canonical), MessageType: types.MessageTypeRaw, SigningAlgorithm: types.SigningAlgorithmSpecEd25519Sha512, }) if err != nil { return "", err } return base64.StdEncoding.EncodeToString(out.Signature), nil } ``` ```java import software.amazon.awssdk.core.SdkBytes; import software.amazon.awssdk.services.kms.KmsClient; import software.amazon.awssdk.services.kms.model.MessageType; import software.amazon.awssdk.services.kms.model.SignRequest; import software.amazon.awssdk.services.kms.model.SignResponse; import software.amazon.awssdk.services.kms.model.SigningAlgorithmSpec; import java.util.Base64; public String computeSignature(KmsClient kmsClient, String keyId, String method, String path, String timestamp, String body) { String canonical = String.format("method=%s&path=%s×tamp=%s&body=%s", method, path, timestamp, body); SignRequest request = SignRequest.builder() .keyId(keyId) .message(SdkBytes.fromUtf8String(canonical)) .messageType(MessageType.RAW) .signingAlgorithm(SigningAlgorithmSpec.ED25519_SHA_512) .build(); SignResponse response = kmsClient.sign(request); return Base64.getEncoder().encodeToString(response.signature().asByteArray()); } ``` ```python import base64 def compute_signature(kms_client, key_id: str, method: str, path: str, timestamp: str, body: str = '') -> str: canonical = f"method={method}&path={path}×tamp={timestamp}&body={body}" response = kms_client.sign( KeyId=key_id, Message=canonical.encode(), MessageType='RAW', SigningAlgorithm='ED25519_SHA_512' ) return base64.b64encode(response['Signature']).decode() ``` ```php sign([ 'KeyId' => $keyId, 'Message' => $canonical, 'MessageType' => 'RAW', 'SigningAlgorithm' => 'ED25519_SHA_512', ]); // $result['Signature'] is already raw bytes, the SDK base64-decodes it for you return base64_encode($result['Signature']); } ``` `kmsClient`/`client`/`$kms` above is whatever KMS client your language's AWS SDK already gives you, configured however you normally configure it. See [Client Key Pair](https://docs.fystack.io/api-keys.md#client-key-pair) for the general local-vs-production pattern (point it at LocalStack/ministack with static test credentials locally, drop both in production and let the IAM role supply credentials), the same idea applies regardless of language, just through that SDK's own config options. --- # Pagination In this guide, we will look at how to work with paginated responses when querying the Fystack API. By default, list responses limit results to 20. You can request up to 1000 items by adding a `limit` parameter. If you are using one of the official Fystack API client libraries, you don't need to worry about pagination, as it's handled behind the scenes. When an API response returns a list of objects, no matter the amount, pagination is supported. In paginated responses, objects are nested in a `data` attribute. You can use the `offset` and `limit` query parameters to browse pages. ## Example using offset pagination In this example, we request the first page of networks with a limit of 10 items. You can navigate through pages by adjusting the offset value. For example, to get the second page, you would use `offset=10` when your `limit` is 10. - `offset` (integer): The number of items to skip before starting to collect the result set. - `limit` (integer): Limit the number of items returned per page. The maximum value is 1000. ```bash curl -G https://api.fystack.io/api/v1/networks \ -d offset=0 \ -d limit=10 ``` ```json { "data": [ { "id": "WAz8eIbvDR60rouK", // ... }, { "id": "hSIhXBhNe8X1d8Et" // ... }, { "id": "fbwYwpi9C2ybt6Yb" // ... } // ... 7 more items ] } ``` --- # Errors In this guide, we will talk about what happens when something goes wrong while you work with the API. Mistakes happen, and mostly they will be yours, not ours. Let's look at some status codes and error types you might encounter. You can tell if your request was successful by checking the status code when receiving an API response. If a response comes back unsuccessful, you can use the error message and code to figure out what has gone wrong and do some rudimentary debugging (before contacting support). > **Note:** Before reaching out to support with an error, please be aware that 99% of all > reported errors are, in fact, user errors. Therefore, please carefully check > your code before contacting Fystack support. --- ## Status codes Here is a list of the different categories of status codes returned by the Fystack API. Use these to understand if a request was successful. - `2xx`: A 2xx status code indicates a successful response. - `4xx`: A 4xx status code indicates a client error — this means it's a _you_ problem. - `5xx`: A 5xx status code indicates a server error — you won't be seeing these. --- ## Error response format Whenever a request is unsuccessful, the Fystack API will return an error response with a structured format. All error responses include a `success` field set to `false`, a descriptive `message`, and an error `code`. For validation errors, you'll also see an `errors` array containing details about which fields failed validation. Here are the common components of an error response: - `success`: Always `false` for error responses. - `message`: A human-readable description of what went wrong. - `code`: An internal error code that can help with debugging. - `errors`: For validation errors, contains details about which fields failed validation and why. ```json { "success": false, "message": "Resource not found", "code": 404 } ``` ```json { "success": false, "message": "validation error", "code": 400, "errors": [ { "field": "email", "reason": "required" }, { "field": "password", "reason": "min=8" } ] } ``` --- ## Error codes The Fystack API uses specific error codes to help identify the exact issue that occurred. These codes provide more granular information than HTTP status codes alone. | Code | Name | Description | | --- | --- | --- | | 2407 | ValidationErrCode | Returned when request data fails validation requirements. | | 2408 | GenerationErrCode | Indicates an error occurred during content or resource generation. | | 2409 | DatabaseErrCode | Returned when a database operation fails. | | 2410 | ConflictErrCode | Indicates a conflict with the current state of the resource. | | 2411 | InfraErrCode | Returned when an infrastructure-related error occurs. | | 2412 | RedisErrCode | Indicates a problem with Redis cache operations. | | 2413 | ForbiddenResourceErrCode | Returned when the user does not have access to the requested resource. | | 2414 | ForbiddenActionErrCode | Indicates the user is not allowed to perform the requested action. | | 2415 | UnauthorizedErrCode | Returned when authentication is required but missing or invalid. | | 2416 | ExecutionErrCode | Indicates an error during the execution of a command or process. | | 2417 | NotFoundErrCode | Returned when the requested resource doesn't exist. | | 2418 | EncodingErrCode | Indicates an error occurred while encoding data. | | 2419 | DecodingErrCode | Returned when there's an error decoding data. | | 2420 | InternalResourceNotFound | Indicates a required internal resource could not be found. | | 2421 | BadRequestErrCode | Returned when the request is malformed or contains invalid parameters. | | 2422 | InternalErrCode | Indicates an unexpected internal server error occurred. | > **Note:** These error codes provide specific information about what went wrong with your request. > When contacting support, always include the error code to help us diagnose issues more quickly. --- # APIs Reference documentation for every resource in the Fystack API, from API keys and wallets to policies and webhooks. - [API Keys](https://docs.fystack.io/api-keys.md): How to get the HMAC secret or generate the Ed25519 client key pair used to sign API requests, with OpenSSL or AWS KMS. - [Wallets](https://docs.fystack.io/wallets.md): How to create and manage wallets using the Fystack API. - [Transfers](https://docs.fystack.io/withdrawals.md): How to initiate and manage transfers from your wallets. - [Webhooks](https://docs.fystack.io/webhooks.md): API reference for Fystack's webhook system. - [Pay](https://docs.fystack.io/checkout.md): Integrate Fystack's Checkout API to create seamless payment experiences for your users. - [Networks](https://docs.fystack.io/networks.md): How to retrieve and manage blockchain networks using the Fystack API. - [Assets](https://docs.fystack.io/assets.md): How to retrieve and query cryptocurrency assets using the Fystack API. - [Workspaces](https://docs.fystack.io/workspaces.md): How to retrieve workspace statistics and analytics using the Fystack API. - [Policies](https://docs.fystack.io/policies.md): How Fystack programmable policies evaluate withdrawals and contract calls, and how to create, validate, approve, and manage policy rules. --- # API Keys Fystack API keys use either an HMAC secret or an Ed25519 client key pair for request signing. This page covers getting the right key material for each signing scheme. See [API Authentication](https://docs.fystack.io/authentication.md) for how signing itself works. > **Info.** Prefer the **Client Key Pair** (Ed25519) scheme for anything beyond quick testing. The HMAC secret is a plaintext credential your application has to hold, while an Ed25519 private key can stay in an HSM or AWS KMS and never has to be exportable at all, see [Client Key Pair](#client-key-pair) below. --- ## HMAC Secret No key generation needed on your side. When you create the API key from the dashboard without registering a public key, Fystack generates the secret for you and shows it once, as a hex-encoded string. Simpler to set up than a client key pair, but the secret is a plaintext credential your application must hold, prefer [Client Key Pair](#client-key-pair) below where you can. > **Warning.** Copy `api_secret` immediately and store it securely, it cannot be retrieved again after creation. If it's lost, delete the key and create a new one. ```typescript import { FystackSDK, Environment } from '@fystack/sdk' const sdk = new FystackSDK({ credentials: { apiKey: 'YOUR_API_KEY', apiSecret: 'YOUR_API_SECRET' }, workspaceId: 'YOUR_WORKSPACE_ID', environment: Environment.Production }) ``` --- ## Client Key Pair With the Ed25519 scheme, the private key never leaves your process, only the matching PEM public key is registered with Fystack when you create the API key from the dashboard. Generate the pair with whichever tool fits your setup, then paste the public key into the dashboard. _Image: Create API Key dialog with Client Key Pair selected as the recommended authentication method, using an Ed25519 public key with no secret stored_ > **Info.** For production, generate the key in a hardware-backed store like AWS KMS or an on-prem HSM rather than as a PEM file. The private key is then non-exportable, it never touches application memory or disk, and it can't be exfiltrated even if the host is compromised. Use `AwsKmsSigner` (below) for this, reach for `LocalPrivateKeySigner` and a PEM file only for local development and testing. ### Using the SDK If you're using `@fystack/sdk`, pass a `signer` on `credentials` instead of a raw key, it builds the canonical string and signs each request for you. Use `LocalPrivateKeySigner` for an in-process PEM key, or `AwsKmsSigner` when the private key lives in AWS KMS: ```typescript import { FystackSDK, LocalPrivateKeySigner, Environment } from '@fystack/sdk' const sdk = new FystackSDK({ credentials: { apiKey: 'YOUR_API_KEY', signer: new LocalPrivateKeySigner('-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----') }, workspaceId: 'YOUR_WORKSPACE_ID', environment: Environment.Production }) ``` `AwsKmsSigner` requires `@aws-sdk/client-kms` to be installed (it's loaded lazily), or pass a preconfigured client via `client` instead of `clientConfig`. Its `clientConfig` is passed straight to `new KMSClient(...)`, so it differs between local development against LocalStack/ministack and production: ```typescript import { FystackSDK, AwsKmsSigner, Environment } from '@fystack/sdk' const sdk = new FystackSDK({ credentials: { apiKey: API_KEY, signer: new AwsKmsSigner({ keyId: 'alias/signer', clientConfig: { region: 'ap-southeast-1', endpoint: 'http://localhost:4566', credentials: { accessKeyId: 'test', secretAccessKey: 'test' } } }) }, environment: Environment.Production, debug: true }) ``` ```typescript import { FystackSDK, AwsKmsSigner, Environment } from '@fystack/sdk' const sdk = new FystackSDK({ credentials: { apiKey: API_KEY, // Drop endpoint and credentials, the KMS client picks up the // task/instance IAM role and the real region automatically. signer: new AwsKmsSigner({ keyId: 'alias/signer', clientConfig: { region: 'ap-southeast-1' } }) }, environment: Environment.Production, debug: true }) ``` > **Warning.** Never hardcode `accessKeyId` / `secretAccessKey` in production, that pair in the LocalStack example is a fixed local-only credential. Let the runtime's IAM role supply credentials instead. Not on Node.js? See [Ed25519 Signing](https://docs.fystack.io/authentication.md#ed25519-signing) for signing the canonical string directly, with both a local PEM key and AWS KMS, in Go, Python, PHP, and Java. ### Generating a key pair with OpenSSL ```bash openssl genpkey -algorithm ed25519 -out private.pem openssl pkey -in private.pem -pubout -out public.pem ``` Paste the contents of `public.pem` into the dashboard when creating the API key, and keep `private.pem` for signing, pass it to the SDK as `new LocalPrivateKeySigner(privateKeyPem)`. ### Generating a key pair with AWS KMS AWS KMS supports Ed25519 as an asymmetric signing key (`ECC_NIST_EDWARDS25519`). Create the key, then export the public half: ```bash KEY_ID=$(awslocal kms create-key \ --key-spec ECC_NIST_EDWARDS25519 --key-usage SIGN_VERIFY \ --query KeyMetadata.KeyId --output text) echo "$KEY_ID" ``` `get-public-key` already returns the key as base64-encoded DER (`SubjectPublicKeyInfo`), which is exactly what a PEM public key wraps, so there's no need to pipe it through `openssl` to reformat it. Wrap the base64 output in PEM headers directly: ```bash { echo "-----BEGIN PUBLIC KEY-----" awslocal kms get-public-key --key-id alias/signer \ --query PublicKey --output text echo "-----END PUBLIC KEY-----" } > pub.pem ``` > **Info.** An Ed25519 public key is only 32 bytes, so the base64 output is short enough to stay on one line. For longer keys you'd need to wrap it at 64 characters (`fold -w 64`) to produce valid PEM, Ed25519 keys don't need that step. The equivalent, longer route (decode to DER, then let `openssl` re-encode as PEM) produces an identical file: ```bash awslocal kms get-public-key --key-id alias/signer \ --query PublicKey --output text | base64 -d > pub.der openssl pkey -pubin -inform DER -in pub.der -out pub.pem ``` Either way you end up with: ```text -----BEGIN PUBLIC KEY----- MCowBQYDK2VwAyEAKZ0GCR0MBIQGZowZI/1xmWjXlBy09I98JC6Tgn0l0L4= -----END PUBLIC KEY----- ``` Paste that into the dashboard as the key's public key. The private key stays in KMS, use `new AwsKmsSigner({ keyId: 'alias/signer' })` as shown above to sign requests, KMS never exposes the private key material. --- # Wallets Wallets are secure containers for managing digital assets. Fystack offers different wallet types to suit your needs, including Hyper wallets and MPC (Multi-Party Computation) wallets. ## Wallet types Fystack supports two distinct wallet types, each designed for specific use cases and security requirements: ### Hyper Wallet Hyper wallets are derived using Hierarchical Deterministic (HD) key derivation per workspace. This approach provides: - **Efficient key management**: Uses a single master seed to derive multiple wallet keys - **End-user focused**: Ideal for generating individual wallets for your end users - **Quick creation**: Instant wallet generation with immediate `success` status - **Deterministic**: Wallets can be recreated from the same seed if needed - **Cost-effective**: Lower computational overhead for high-volume wallet creation **Best for**: Consumer applications, user wallets, high-volume wallet generation ### MPC Wallet MPC (Multi-Party Computation) wallets use advanced cryptographic techniques where the private key is split into multiple shares and distributed across different Mpcium nodes: - **Enhanced security**: Private key never exists in its complete form on any single node - **Distributed trust**: Key shares are distributed across multiple secure nodes - **Hot wallet protection**: Provides institutional-grade security for active wallets - **Threshold signatures**: Requires consensus from multiple nodes to sign transactions - **Longer creation time**: Key generation process takes more time due to cryptographic complexity **Best for**: Hot wallets, institutional custody, high-value asset storage, exchange wallets ### Choosing the right wallet type | Wallet Type | Hyper | MPC | | --- | --- | --- | | Security Level | High | Institutional-grade | | Creation Speed | Instant | 7-15 seconds | | Use Case | End-user wallets | Hot wallets, custody | | Key Management | HD derivation | Multi-party computation | | Cost | Lower | Higher | | Recovery | HD seed-based | Multi-party recovery | Choose **Hyper** wallets when you need to quickly generate wallets for individual users. Choose **MPC** wallets when you need maximum security for operational wallets that hold significant value or require institutional-grade protection. --- ## Create a wallet **Endpoint:** `POST /wallets` To create a new wallet, send a POST request to the `/wallets` endpoint with the required parameters. ### Required parameters - `name` (string): A descriptive name for the wallet. - `wallet_type` (string): The type of wallet to create. Must be either `standard` or `mpc`. ### Optional parameters - `wallet_purpose` (string): The purpose of the wallet. Must be one of `general`, `user`, `gas_tank`, `payment`, or `one_time_use`. If set to `user`, the wallet will be hidden from the portal. Defaults to `general`. - `gas_sponsorship_enabled` (boolean): Sponsor gas for token transfers from this wallet on EVM, Solana, and TRON networks, so the wallet does not need to hold the native coin. Gas is paid from the workspace gas tank, see [Gas Station](https://docs.fystack.io/product/gas-station.md). Disabled by default; it can also be changed later from the wallet settings. - `sweep_task_id` (string): UUID of an existing [sweep task](https://docs.fystack.io/product/automation.md#sweep-tasks) to attach to this wallet. Create the sweep task first, then copy its ID from the Automation page in the dashboard. ### Sweep tasks To sweep funds out of new wallets automatically, attach a sweep task when you create them: 1. **Create a sweep task** under **Automation > Add Sweep Task** in the dashboard. See [Automation](https://docs.fystack.io/product/automation.md#sweep-tasks). 2. **Copy the sweep task ID** from the Automation page. 3. **Pass it as `sweep_task_id`** when creating each wallet. #### How sweep tasks work - **Automatic monitoring**: The system continuously monitors the wallet balance - **Threshold triggering**: When the wallet's USD value reaches the task's minimum trigger value, a sweep is triggered - **Destination flexibility**: Funds can be swept to internal wallets or external addresses from your address book - **User wallets**: Wallets with `wallet_purpose: "user"` are hidden from the portal interface > **Info: Reuse one sweep task across wallets.** Attach the same `sweep_task_id` to every wallet that should sweep to the same destination. Updating that one task then applies to every attached wallet. ### Response - `success` (boolean): Indicates whether the request was successful. - `message` (string): A message describing the result of the operation. - `code` (integer): A numeric code indicating the result status (0 for success). - `data` (object): Contains the wallet details. - `wallet_id` (string): The UUID of the newly created wallet. - `status` (string): The current status of the wallet creation process. **Request: Basic wallet** `POST /wallets` ```typescript import { FystackSDK, Environment } from "@fystack/sdk" const apiCredentials = { apiKey: process.env.API_KEY, apiSecret: process.env.API_SECRET } const sdk = new FystackSDK({ credentials: apiCredentials, environment: Environment.Sandbox, logger: true }) await sdk.createWallet({ name: 'Treasury wallet', walletType: WalletType.MPC }) ``` ```bash curl -X POST https://api.fystack.io/api/v1/wallets \ -H "ACCESS-API-KEY: your_api_key" \ -H "ACCESS-TIMESTAMP: 1667836889" \ -H "ACCESS-SIGN: YourBase64EncodedSignature==" \ -H "Content-Type: application/json" \ -d '{ "name": "Treasury wallet", "wallet_type": "mpc" }' ``` **Request: With sweep task** `POST /wallets` ```typescript import { FystackSDK, Environment } from "@fystack/sdk" const apiCredentials = { apiKey: process.env.API_KEY, apiSecret: process.env.API_SECRET } const sdk = new FystackSDK({ credentials: apiCredentials, environment: Environment.Sandbox, logger: true }) await sdk.createWallet({ name: 'User deposit wallet', walletType: WalletType.Standard, walletPurpose: WalletPurpose.User, sweepTaskId: '987f6543-e21c-43b2-a654-321098765432' }) ``` ```bash curl -X POST https://api.fystack.io/api/v1/wallets \ -H "ACCESS-API-KEY: your_api_key" \ -H "ACCESS-TIMESTAMP: 1667836889" \ -H "ACCESS-SIGN: YourBase64EncodedSignature==" \ -H "Content-Type: application/json" \ -d '{ "name": "User deposit wallet", "wallet_type": "standard", "wallet_purpose": "user", "sweep_task_id": "987f6543-e21c-43b2-a654-321098765432" }' ``` ```json { "success": true, "message": "success", "code": 0, "data": { "wallet_id": "c2b71329-34c9-45aa-89d3-ddde8b15dec5", "status": "pending" } } ``` --- ## Wallet creation status **Endpoint:** `GET /wallets/creation-status/:wallet_id` When you create a wallet, the process happens asynchronously. The initial response will include a `status` field with one of the following values: - `pending`: The wallet creation process has started but is not yet complete. - `success`: The wallet has been successfully created. - `error`: There was an error creating the wallet. > **Warning.** For `standard` wallet types, the status will be `success` immediately after creation. > **Info.** For `mpc` wallet types, the creation process may take longer as it involves secure key generation across multiple parties. ### Response - `wallet_id` (string): The UUID of the wallet being created. - `status` (string): Current status of the wallet creation process. Can be `pending`, `success`, or `error`. **Request** `GET /wallets/creation-status/:wallet_id` ```bash curl -X GET https://api.fystack.io/api/v1/wallets/creation-status/c2b71329-34c9-45aa-89d3-ddde8b15dec5 \ -H "ACCESS-API-KEY: your_api_key" \ -H "ACCESS-TIMESTAMP: 1667836889" \ -H "ACCESS-SIGN: YourBase64EncodedSignature==" ``` ```json { "wallet_id": "c2b71329-34c9-45aa-89d3-ddde8b15dec5", "status": "pending" } ``` --- ## List all wallets **Endpoint:** `GET /workspaces/:workspace_id/wallets` This endpoint allows you to retrieve a paginated list of all wallets in your workspace. ### Optional parameters - `wallet_type` (string): Filter wallets by type. - `limit` (integer): Limit the number of wallets returned. - `offset` (integer): Number of wallets to skip before starting to return results. **Request** `GET /workspaces/:workspace_id/wallets` ```typescript import { FystackSDK, Environment } from "@fystack/sdk" const apiCredentials = { apiKey: process.env.API_KEY, apiSecret: process.env.API_SECRET } const sdk = new FystackSDK({ credentials: apiCredentials, environment: Environment.Sandbox, logger: true }) const wallets = await sdk.getWallets() console.log('wallets', wallets) ``` ```bash curl -X GET https://api.fystack.io/api/v1/workspaces/123e4567-e89b-12d3-a456-426614174000/wallets \ -H "ACCESS-API-KEY: your_api_key" \ -H "ACCESS-TIMESTAMP: 1667836889" \ -H "ACCESS-SIGN: YourBase64EncodedSignature==" ``` ```json [ { "id": "7f00773a-abbd-434b-b322-7b4016b3ff4a", "name": "MPC wallet", "role": "wallet_admin", "wallet_type": "mpc", "value_usd": "0", "top_assets": [], "wallet_purpose": "general" }, { "id": "8a91b223-4c9f-5ffc-0f1g-235787cfc3f1", "name": "Hyper wallet", "role": "wallet_admin", "wallet_type": "standard", "value_usd": "0", "top_assets": [], "wallet_purpose": "general" } ] ``` ## Get deposit address **Endpoint:** `GET /wallets/:wallet_id/deposit-address` This endpoint returns a deposit address for the specified wallet and asset. ### Query parameters - `asset_id` (string): The UUID of the asset to get the deposit address for. - `address_type` (string): The type of blockchain address to generate. Must be one of: `evm`, `sol`, `tron`, or `btc`. ### Response - `success` (boolean): Indicates whether the request was successful. - `data` (object): Contains the deposit address details. - `asset_id` (string): The UUID of the asset. - `address` (string): The blockchain address for deposits. - `qr_code` (string): URL to a QR code image of the deposit address. > **Info.** If `address_type` is omitted, Fystack selects the address type from the requested asset when possible. **Request** `GET /wallets/:wallet_id/deposit-address` ```typescript import { FystackSDK, Environment } from "@fystack/sdk" const apiCredentials = { apiKey: process.env.API_KEY, apiSecret: process.env.API_SECRET } const sdk = new FystackSDK({ credentials: apiCredentials, environment: Environment.Sandbox, logger: true }) const response = await sdk.getDepositAddress( "c2b71329-34c9-45aa-89d3-ddde8b15dec5", AddressType.Evm ) // Get Solana deposit address const response = await sdk.getDepositAddress( "c2b71329-34c9-45aa-89d3-ddde8b15dec5", AddressType.Solana ) // Get Tron deposit address const response = await sdk.getDepositAddress( "c2b71329-34c9-45aa-89d3-ddde8b15dec5", AddressType.Tron ) ``` ```bash curl -X GET "https://api.fystack.io/api/v1/wallets/c2b71329-34c9-45aa-89d3-ddde8b15dec5/deposit-address?address_type=evm" \ -H "ACCESS-API-KEY: your_api_key" \ -H "ACCESS-TIMESTAMP: 1667836889" \ -H "ACCESS-SIGN: YourBase64EncodedSignature==" ``` ```json { "success": true, "message": "success", "code": 0, "data": { "address": "0xA793457d84CFD255Ed68295984300C4f799E759F", "qr_code": "https://quickchart.io/qr?size=300&margin=2&text=0xA793457d84CFD255Ed68295984300C4f799E759F" } } ``` --- ## Get wallet asset overview **Endpoint:** `GET /wallets/:wallet_id/overview` This endpoint returns wallet assets with balances, active holds, USD prices, and network details. Use it to display a wallet portfolio. Hidden wallet assets and disabled networks are excluded. ### Path parameters - `wallet_id` (string (uuid)): The UUID of the wallet. ### Optional query parameters - `offset` (integer): Number of records to skip. Must be between 0 and 100000. - `limit` (integer): Maximum number of records to return. Must be between 0 and 1000. Omit this parameter or set it to 0 to return all matching records. - `is_native_only` (boolean): Set to `true` to return only native assets. - `search` (string): Search by asset name or symbol. Maximum 100 characters. - `sort_by` (string): Sort by `balance`, `price_usd`, or `value_usd`. When omitted, assets are sorted by name. - `sort_order` (string): Sort direction: `asc` or `desc`. Defaults to `asc` when `sort_by` is provided. - `min_value_usd` (number): Only return assets whose total USD value is at least this amount. Must be 0 or greater. - `network_ids` (array of strings (uuid)): Filter by network UUIDs. Repeat the query parameter to include multiple networks. ### Response - `success` (boolean): Indicates whether the request was successful. - `message` (string): A message describing the result of the operation. - `code` (integer): A numeric code indicating the result status (0 for success). - `data` (array): The matching wallet assets. Returns an empty array when no assets match. - `balance` (string): The total asset balance, returned as a decimal string. - `on_hold` (string): The amount reserved by active holds, returned as a decimal string. - `available_balance` (string): The balance available to use: total balance minus active holds. - `price_usd` (string): The USD price per unit of the asset, returned as a decimal string. - `value_usd` (string): The total balance multiplied by the USD price, returned as a decimal string. - `asset` (object): The asset details, including its UUID, name, symbol, decimals, and network ID. See [Assets](https://docs.fystack.io/assets.md) for asset fields. - `network` (object): The network details: `id`, `name`, `chain_id`, `logo_url`, and `explorer_tx` when available. **Request** `GET /wallets/:wallet_id/overview` ```bash curl -X GET "https://api.fystack.io/api/v1/wallets/c2b71329-34c9-45aa-89d3-ddde8b15dec5/overview?sort_by=value_usd&sort_order=desc&limit=20" \ -H "ACCESS-API-KEY: your_api_key" \ -H "ACCESS-TIMESTAMP: 1667836889" \ -H "ACCESS-SIGN: YourBase64EncodedSignature==" ``` ```json { "success": true, "message": "success", "code": 0, "data": [ { "balance": "125", "on_hold": "25", "available_balance": "100", "asset": { "id": "bf130274-e80c-43f3-aac3-c7dbfcbd74cd", "name": "USDC (Binance-Peg USD Coin)", "symbol": "USDC", "decimals": 18, "logo_url": "https://bscscan.com/token/images/centre-usdc_28.png", "address": "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d", "asset_type": "erc20", "is_native": false, "address_type": "evm", "network_id": "c03b242f-5091-499c-86c5-78dbc47ed719", "price_usd": "1" }, "price_usd": "1", "value_usd": "125", "network": { "id": "c03b242f-5091-499c-86c5-78dbc47ed719", "name": "BNB Mainnet", "chain_id": 56, "logo_url": "https://s2.coinmarketcap.com/static/img/coins/64x64/1839.png", "explorer_tx": "https://bscscan.com/tx/%s" } } ] } ``` --- ## Get wallet balances **Endpoint:** `GET /wallets/:wallet_id/balances` This endpoint returns a flat list of wallet asset balances with asset and network identifiers. Use it to look up balances by exact symbol, asset ID, or network. Hidden wallet assets and disabled networks are excluded. ### Path parameters - `wallet_id` (string (uuid)): The UUID of the wallet. ### Optional query parameters - `offset` (integer): Number of records to skip. Must be between 0 and 100000. - `limit` (integer): Maximum number of records to return. Must be between 0 and 1000. Omit this parameter or set it to 0 to return all matching records. - `symbol` (array of strings): Filter by exact asset symbols, such as `USDC`. Matching is case-insensitive. Each symbol must contain 1 to 20 alphanumeric characters. - `asset_id` (array of strings (uuid)): Filter by asset UUIDs. - `network_id` (array of strings (uuid)): Filter by network UUIDs. - `network_code` (array of strings): Filter by exact network codes, such as `BSC_MAINNET`. Matching is case-insensitive. Each code must contain 1 to 50 characters. Use the `internal_code` from [Networks](https://docs.fystack.io/networks.md). > **Info.** Repeat a query parameter to supply multiple values, for example `symbol=USDC&symbol=USDT`. Values within the same filter match any of those values. Different filters are combined, so a symbol and network filter must both match. ### Response - `success` (boolean): Indicates whether the request was successful. - `message` (string): A message describing the result of the operation. - `code` (integer): A numeric code indicating the result status (0 for success). - `data` (array): The matching wallet assets. Returns an empty array when no assets match. - `wallet_id` (string (uuid)): The UUID of the wallet. - `asset_id` (string (uuid)): The UUID of the asset. - `symbol` (string): The asset symbol. - `asset_name` (string): The asset name. - `asset_logo` (string): The URL of the asset logo. - `network_id` (string (uuid)): The UUID of the network. - `network_code` (string): The internal code of the network. - `network_name` (string): The network name. - `network_logo` (string): The URL of the network logo. - `balance` (string): The total asset balance, returned as a decimal string. - `on_hold` (string): The amount reserved by active holds, returned as a decimal string. - `available_balance` (string): The balance available to use: total balance minus active holds. - `price_usd` (string): The USD price per unit of the asset, returned as a decimal string. - `value_usd` (string): The total balance multiplied by the USD price, returned as a decimal string. **Request** `GET /wallets/:wallet_id/balances` ```bash curl -X GET "https://api.fystack.io/api/v1/wallets/c2b71329-34c9-45aa-89d3-ddde8b15dec5/balances?symbol=USDC&symbol=USDT&network_code=BSC_MAINNET&limit=20" \ -H "ACCESS-API-KEY: your_api_key" \ -H "ACCESS-TIMESTAMP: 1667836889" \ -H "ACCESS-SIGN: YourBase64EncodedSignature==" ``` ```json { "success": true, "message": "success", "code": 0, "data": [ { "wallet_id": "c2b71329-34c9-45aa-89d3-ddde8b15dec5", "asset_id": "bf130274-e80c-43f3-aac3-c7dbfcbd74cd", "symbol": "USDC", "asset_name": "USDC (Binance-Peg USD Coin)", "asset_logo": "https://bscscan.com/token/images/centre-usdc_28.png", "network_id": "c03b242f-5091-499c-86c5-78dbc47ed719", "network_code": "BSC_MAINNET", "network_name": "BNB Mainnet", "network_logo": "https://s2.coinmarketcap.com/static/img/coins/64x64/1839.png", "balance": "125", "on_hold": "25", "available_balance": "100", "price_usd": "1", "value_usd": "125" } ] } ``` --- ## Get wallet balance by asset ID **Endpoint:** `GET /wallets/:wallet_id/balance/:asset_id` This endpoint returns the total balance of a single asset in the wallet. To retrieve active holds and the available balance, use [Get wallet balances](#get-wallet-balances). ### Path parameters - `wallet_id` (string (uuid)): The UUID of the wallet. - `asset_id` (string (uuid)): The UUID of the asset. ### Response - `success` (boolean): Indicates whether the request was successful. - `message` (string): A message describing the result of the operation. - `code` (integer): A numeric code indicating the result status (0 for success). - `data` (string): The total balance for the requested asset, returned as a decimal string. **Request** `GET /wallets/:wallet_id/balance/:asset_id` ```bash curl -X GET "https://api.fystack.io/api/v1/wallets/c2b71329-34c9-45aa-89d3-ddde8b15dec5/balance/bf130274-e80c-43f3-aac3-c7dbfcbd74cd" \ -H "ACCESS-API-KEY: your_api_key" \ -H "ACCESS-TIMESTAMP: 1667836889" \ -H "ACCESS-SIGN: YourBase64EncodedSignature==" ``` ```json { "success": true, "message": "success", "code": 0, "data": "125" } ``` --- # Transfers This guide explains how to transfer funds from your wallets to external addresses. --- ## Request withdrawal **Endpoint:** `POST /wallets/:wallet_id/request-withdrawal` Create a new withdrawal request from a wallet. The withdrawal will require approval from authorized users. ### Headers - `X-Idempotency-Key` (header): Optional. A UUID that identifies this withdrawal so it is processed only once. If the same key is sent again within 24 hours, the cached response from the original request is returned instead of creating a duplicate withdrawal. Tie the key to the withdrawal in your own system: use your internal withdrawal or transfer ID if it is a UUID, otherwise generate a UUID once when you create that record and store it alongside. Send the same key on every retry. Do not generate a new key per attempt, because each new key is treated as a new withdrawal. ### Required parameters - `asset_id` (string): The UUID of the asset to withdraw. - `amount` (string): The amount to withdraw in decimal string format. - `recipient_address` (string): The blockchain address to send funds to. ### Response - `id` (string): The UUID of the withdrawal request. - `status` (string): Current status of the withdrawal: - `PENDING_APPROVAL`: Waiting for approvals from authorized users - `PENDING`: Approved and waiting to be processed - `EXECUTED`: Transaction has been sent to the blockchain - `SUCCESS`: Transaction has been confirmed on the blockchain - `REJECTED`: Withdrawal was rejected by an approver - `FAILED`: Transaction failed to execute - `wallet_id` (string): The UUID of the source wallet. - `asset_id` (string): The UUID of the asset being withdrawn. - `amount` (string): The withdrawal amount. - `recipient_address` (string): The destination blockchain address. - `creator_id` (string): The UUID of the user who created the withdrawal request. - `withdrawal_approvals` (array): List of approval statuses from authorized users. - `created_at` (string): Timestamp of when the request was created. - `updated_at` (string): Timestamp of the last status update. > **Info.** The withdrawal process follows this flow: > > 1. PENDING_APPROVAL → Waiting for required approvals > 2. PENDING → Approved and queued for processing > 3. EXECUTED → Transaction sent to blockchain > 4. SUCCESS → Transaction confirmed > > Or if rejected/failed: > > - REJECTED → An approver rejected the withdrawal > - FAILED → Transaction failed to execute **Request** `POST /wallets/:wallet_id/request-withdrawal` ```typescript import { FystackSDK, Environment } from "@fystack/sdk" const apiCredentials = { apiKey: process.env.API_KEY, apiSecret: process.env.API_SECRET } const sdk = new FystackSDK({ credentials: apiCredentials, environment: Environment.Sandbox, logger: true }) const response = await sdk.requestWithdrawal( "e0851607-417a-475a-b399-5f3aa262b81d", { assetId: "f1be472d-1338-43ff-b547-06faba7ecce7", amount: "0.001", recipientAddress: "0xe6EBF81E9C225BbCEa9b5399E0D0d0f29f30f119" } ) ``` ```bash curl -X POST https://api.fystack.io/api/v1/wallets/e0851607-417a-475a-b399-5f3aa262b81d/request-withdrawal \ -H "ACCESS-API-KEY: your_api_key" \ -H "ACCESS-TIMESTAMP: 1667836889" \ -H "ACCESS-SIGN: YourBase64EncodedSignature==" \ -H "X-Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000" \ -H "Content-Type: application/json" \ -d '{ "asset_id": "f1be472d-1338-43ff-b547-06faba7ecce7", "amount": "0.001", "recipient_address": "0xe6EBF81E9C225BbCEa9b5399E0D0d0f29f30f119" }' ``` ```json { "success": true, "message": "success", "code": 0, "data": { "id": "0de79230-a820-4bd4-b020-883d395faa87", "wallet_id": "e0851607-417a-475a-b399-5f3aa262b81d", "asset_id": "f1be472d-1338-43ff-b547-06faba7ecce7", "amount": "1000", "recipient_address": "0x7c2BbE2e02af09F1cf9aFd4A79EC201DC841675a", "status": "PENDING_APPROVAL", "creator_id": "075243c1-962e-4803-83da-d73c78b8b060", "withdrawal_approvals": [ { "id": "9a85c312-7d3b-4c6f-9f4e-123456789abc", "withdrawal_id": "0de79230-a820-4bd4-b020-883d395faa87", "user_id": "075243c1-962e-4803-83da-d73c78b8b060", "status": "PENDING", "created_at": "2025-08-11T17:37:29.815148+07:00", "updated_at": "2025-08-11T17:37:29.815148+07:00" }, { "id": "8b74d123-9e5f-4a2d-8c3b-987654321def", "withdrawal_id": "0de79230-a820-4bd4-b020-883d395faa87", "user_id": "165243c1-962e-4803-83da-d73c78b8b070", "status": "APPROVED", "created_at": "2025-08-11T17:37:29.815148+07:00", "updated_at": "2025-08-11T17:38:15.623147+07:00" } ], "created_at": "2025-08-11T17:37:29.815148+07:00", "updated_at": "2025-08-11T17:37:29.815148+07:00" } } ``` --- ## Get withdrawal status **Endpoint:** `GET /wallets/:wallet_id/withdrawals/:withdrawal_id` Get the current status of a withdrawal request. ### Response - `success` (boolean): Indicates whether the request was successful. - `data` (object): Contains the withdrawal details. - `id` (string): The UUID of the withdrawal request. - `status` (string): Current status of the withdrawal. - `amount` (string): The withdrawal amount. - `recipient_address` (string): The destination blockchain address. - `creator` (object): Information about the user who created the withdrawal. - `asset` (object): Details about the asset being withdrawn. - `transaction` (object): Complete transaction details once executed. **Request** `GET /wallets/:wallet_id/withdrawals/:withdrawal_id` ```typescript import { FystackSDK, Environment } from "@fystack/sdk" const apiCredentials = { apiKey: process.env.API_KEY, apiSecret: process.env.API_SECRET } const sdk = new FystackSDK({ credentials: apiCredentials, environment: Environment.Sandbox, logger: true }) // Get withdrawal status const withdrawal = await sdk.getWithdrawalStatus( "e0851607-417a-475a-b399-5f3aa262b81d", "4cebc6c3-1e46-4166-ab59-86a59971215f" ) // Check withdrawal status if (withdrawal.status === 'SUCCESS') { console.log('Transaction hash:', withdrawal.transaction.tx_hash) console.log('Amount:', withdrawal.amount) console.log('Asset:', withdrawal.asset.symbol) } // Check approval status const approvals = withdrawal.withdrawal_approvals const pendingApprovals = approvals.filter(a => a.status === 'PENDING') const approvedCount = approvals.filter(a => a.status === 'APPROVED').length console.log('Pending approvals:', pendingApprovals.length) console.log('Approved count:', approvedCount) ``` ```bash curl -X GET https://api.fystack.io/api/v1/wallets/e0851607-417a-475a-b399-5f3aa262b81d/withdrawals/4cebc6c3-1e46-4166-ab59-86a59971215f \ -H "ACCESS-API-KEY: your_api_key" \ -H "ACCESS-TIMESTAMP: 1667836889" \ -H "ACCESS-SIGN: YourBase64EncodedSignature==" ``` ```json { "success": true, "message": "success", "code": 0, "data": { "id": "4cebc6c3-1e46-4166-ab59-86a59971215f", "created_at": "2025-07-29T21:44:23.763113+07:00", "updated_at": "2025-07-29T21:45:07.204964+07:00", "amount": "0.000001622515351558", "status": "SUCCESS", "recipient_address": "0xf2acfF1D9d576E9b8B7C7d58A449F599A9A54edA", "creator_id": "075243c1-962e-4803-83da-d73c78b8b060", "creator": { "id": "075243c1-962e-4803-83da-d73c78b8b060", "username": "Dev team" }, "asset_id": "a469642e-5466-4d69-834d-537f33ee5c81", "asset": { "id": "a469642e-5466-4d69-834d-537f33ee5c81", "name": "Ethereum Sepolia", "symbol": "ETH", "decimals": 18, "logo_url": "https://icons.iconarchive.com/icons/cjdowner/cryptocurrency-flat/512/Ethereum-ETH-icon.png", "is_native": true, "address_type": "evm", "network_id": "23d13c03-1f3c-4e2a-9aef-1771a244b7f6" }, "withdrawal_approvals": [ { "id": "dfa94b32-83f5-4808-8091-9a3c270d4f1d", "status": "PENDING", "user": { "username": "Thi Nguyen", "email": "nguyendanganhthi247@gmail.com" } }, { "id": "776303fd-762e-4593-9f66-93631dd226e0", "status": "APPROVED", "user": { "username": "Dev team", "email": "devteam@fystack.io" } } ], "transaction": { "tx_hash": "0xa1ec8bab32baebad1211e838d655777640e78102b39b008bd00a8b3e5d1f3c00", "status": "confirmed", "block_number": 8868747, "from_address": "0xA793457d84CFD255Ed68295984300C4f799E759F", "to_address": "0xf2acfF1D9d576E9b8B7C7d58A449F599A9A54edA", "amount": "0.000001622515351558", "tx_fee": "0.000001296799854", "price_native_token": "3808.475025265875", "direction": "out", "type": "withdrawal", "method": "transfer" } } } ``` --- # Web3 Sign Sign messages, typed data, and transactions on EVM and Solana networks. **Enable Web3 Signing for the wallet before using these endpoints or the SDK.** It is disabled by default; signing requests return **403 Forbidden** until enabled. See [Enable Web3 signing](https://docs.fystack.io/product/web3-sign.md#enable-web3-signing). Signing requests require approval before execution. Review them under **Approval → DApp Transaction** in the dashboard. See [Web3 signing approvals](https://docs.fystack.io/product/web3-sign.md#review-pending-requests). Requests return a `transaction_id`. Poll the status endpoint until signing completes or fails. --- ## Request a signature **Endpoint:** `POST /web3/:wallet_id/sign` Sign an EVM or Solana message. See [API keys](https://docs.fystack.io/api-keys.md) for authentication. The SDK requires a wallet and an RPC provider for EVM signing. It polls for the signature automatically. ### Parameters - `wallet_id` (string): Wallet ID in the URL path. - `method` (string): `eth_sign`, `eth_signTypedData_v4`, or `solana_signMessage`. - `message` (string): Message to sign. Use an empty string for typed data. - `chain_id` (number): EVM chain ID. Use `0` for Solana. - `typed_data` (string): Required for typed data signing. JSON string containing `domain`, `types`, and `message`. - `X-Idempotency-Key` (string): Optional header. Reuse the same key when retrying a request. The SDK currently does not map `options.idempotencyKey` to this header. ### Response - `transaction_id` (string): Request ID for checking signing status. **Request** `POST /web3/:wallet_id/sign` ```typescript import { EtherSigner, Environment } from '@fystack/sdk' import { JsonRpcProvider } from 'ethers' const provider = new JsonRpcProvider(process.env.RPC_URL!) const signer = new EtherSigner( { apiKey: process.env.API_KEY!, apiSecret: process.env.API_SECRET!, }, Environment.Production, provider, ) signer.setWallet('YOUR_WALLET_ID') const signature = await signer.signMessage('Authorize this session') ``` ```typescript import { EtherSigner, Environment } from '@fystack/sdk' import { JsonRpcProvider } from 'ethers' const provider = new JsonRpcProvider(process.env.RPC_URL!) const signer = new EtherSigner( { apiKey: process.env.API_KEY!, apiSecret: process.env.API_SECRET!, }, Environment.Production, provider, ) signer.setWallet('YOUR_WALLET_ID') const network = await provider.getNetwork() const signature = await signer.signTypedData( { name: 'Example App', version: '1', chainId: network.chainId, }, { Session: [ { name: 'account', type: 'address' }, { name: 'nonce', type: 'string' }, ], }, { account: await signer.getAddress(), nonce: crypto.randomUUID(), }, ) console.log('Typed data signature:', signature) ``` ```typescript import { SolanaSigner, Environment } from '@fystack/sdk' const solanaSigner = new SolanaSigner( { apiKey: process.env.API_KEY!, apiSecret: process.env.API_SECRET!, }, Environment.Production, ) solanaSigner.setWallet('YOUR_WALLET_ID') const signature = await solanaSigner.signMessage('Authorize this session') console.log('Signature:', signature) ``` ```json { "method": "eth_sign", "message": "Authorize this session", "chain_id": 11155111 } ``` ```json { "transaction_id": "0de79230-a820-4bd4-b020-883d395faa87" } ``` --- ## Get signing status **Endpoint:** `GET /web3/:wallet_id/sign/:transaction_id` Get the status and signature for a signing request. - `transaction_id` (string): Signing request ID. - `status` (string): `pending_approval`, `pending`, `completed`, `confirmed`, `failed`, or `rejected`. - `signature` (string): Signature, when available. EVM SDK signers wait for `confirmed` with a signature. - `approvals` (array): Records with `user_id` and `status`: `pending`, `approved`, or `rejected`. - `created_at` (string): Creation timestamp. - `updated_at` (string): Last update timestamp. If polling times out, check the existing request before submitting another. **Request** `GET /web3/:wallet_id/sign/:transaction_id` ```typescript import { APIService, Environment } from '@fystack/sdk' const api = new APIService( { apiKey: process.env.API_KEY!, apiSecret: process.env.API_SECRET! }, Environment.Production, ) const status = await api.getSignStatus('YOUR_WALLET_ID', 'TRANSACTION_ID') console.log(status.signature) ``` ```json { "transaction_id": "0de79230-a820-4bd4-b020-883d395faa87", "status": "pending_approval", "approvals": [], "created_at": "2026-10-03T00:00:00Z", "updated_at": "2026-10-03T00:00:00Z" } ``` --- ## Sign a transaction **Endpoint:** `POST /web3/transaction/:wallet_id/signRaw` > **Warning: Unsafe signing.** Raw signing is not recommended. A malicious request can trick you into signing a transaction that steals funds. Only sign payloads you have verified. Submit a transaction for signing and processing. Returns a `transaction_id`. - **EVM**: `from`, `to`, `nonce`, `gasLimit`, `data`, `value`, `chainId`, `maxFeePerGas`, `maxPriorityFeePerGas`, and `accessList`. - **Solana**: `data` (base64 serialized transaction), `from`, `meta.tx_method` (`solana_signTransaction`), and `chainId` (`"1399811149"`). The EVM SDK's `signTransaction` returns a transaction hash. `sendTransaction` populates transaction fields and returns a transaction response. **Request** `POST /web3/transaction/:wallet_id/signRaw` ```typescript import { EtherSigner, Environment } from '@fystack/sdk' import { JsonRpcProvider, parseEther } from 'ethers' const provider = new JsonRpcProvider(process.env.RPC_URL!) const signer = new EtherSigner( { apiKey: process.env.API_KEY!, apiSecret: process.env.API_SECRET!, }, Environment.Production, provider, ) signer.setWallet('YOUR_WALLET_ID') const tx = await signer.sendTransaction({ to: '0x7c2BbE2e02af09F1cf9aFd4A79EC201DC841675a', value: parseEther('0.01'), }) console.log(tx.hash) ``` ```typescript import { SolanaSigner, Environment } from '@fystack/sdk' const solanaSigner = new SolanaSigner( { apiKey: process.env.API_KEY!, apiSecret: process.env.API_SECRET!, }, Environment.Production, ) solanaSigner.setWallet('YOUR_WALLET_ID') // Base64 serialized transaction from @solana/web3.js. const signature = await solanaSigner.signTransaction(serializedTransaction) console.log(signature) ``` ```json { "transaction_id": "0de79230-a820-4bd4-b020-883d395faa87" } ``` --- ## Get transaction status **Endpoint:** `GET /web3/transaction/:wallet_id/:transaction_id` Returns `transaction_id`, `status`, `method`, `approvals`, `created_at`, and `updated_at`, with `hash` and `failed_reason` when available. SDK transaction signers wait for `confirmed` or `completed` with a `hash`, and stop on `failed` or `rejected`. **Request** `GET /web3/transaction/:wallet_id/:transaction_id` ```typescript import { APIService, Environment } from '@fystack/sdk' const api = new APIService( { apiKey: process.env.API_KEY!, apiSecret: process.env.API_SECRET! }, Environment.Production, ) const status = await api.getTransactionStatus('YOUR_WALLET_ID', 'TRANSACTION_ID') console.log(status.hash) ``` ```json { "transaction_id": "0de79230-a820-4bd4-b020-883d395faa87", "status": "pending", "method": "solana_signTransaction", "approvals": [], "created_at": "2026-10-03T00:00:00Z", "updated_at": "2026-10-03T00:00:00Z" } ``` --- # Webhooks Webhooks allow you to receive real-time notifications when specific events occur in your Fystack account. --- ## Get webhook public key **Endpoint:** `GET /workspaces/:workspace_id/webhook-verification-key` Retrieve the public key used to verify webhook signatures for your workspace. Each webhook request is signed using Ed25519, and you should verify this signature before processing the webhook data. ### Response - `success` (boolean): Indicates whether the request was successful. - `message` (string): A message describing the result of the operation. - `code` (integer): A numeric code indicating the result status (0 for success). - `data` (object): Contains the public key details. - `public_key` (string): The Ed25519 public key for webhook signature verification. > **Warning.** Store this public key, you'll need it to verify webhook signatures. **Request** `GET /workspaces/:workspace_id/webhook-verification-key` ```typescript import { FystackSDK, Environment } from "@fystack/sdk" const apiCredentials = { apiKey: process.env.API_KEY, apiSecret: process.env.API_SECRET } const sdk = new FystackSDK({ credentials: apiCredentials, environment: Environment.Sandbox, logger: true }) const response = await sdk.getWebhookPublicKey( "workspace_id" ) ``` ```bash curl -X GET https://api.fystack.io/api/v1/workspaces/workspace_id/webhook-verification-key \ -H "ACCESS-API-KEY: your_api_key" \ -H "ACCESS-TIMESTAMP: 1667836889" \ -H "ACCESS-SIGN: YourBase64EncodedSignature==" ``` ```json { "success": true, "message": "success", "code": 0, "data": { "public_key": "503d18d8375e60667a4cdb879e72c249b3dc054e7d6443c5ccd93aef7f6547fb" } } ``` --- ## Webhook Events Fystack sends webhook notifications for various events. Each event type represents a specific state change in your account. ### Deposit Events - `deposit.pending`: A new deposit has been detected but not yet confirmed on the blockchain. - `deposit.confirmed`: A deposit has been confirmed on the blockchain. > **Warning.** Deposit event payloads include an `idempotency_key` field. Use it to safely deduplicate webhook deliveries, see [Idempotency Key](#idempotency-key) below. ### Withdrawal Events - `withdrawal.pending`: A withdrawal request is pending approval. - `withdrawal.executed`: The withdrawal transaction has been sent to the blockchain. - `withdrawal.confirmed`: The withdrawal transaction has been confirmed on the blockchain. - `withdrawal.failed`: The withdrawal has failed. The payload is the withdrawal object with `status: "FAILED"`. The nested `transaction` object is optional and may be `null`. > **Info.** When a withdrawal fails, the held funds are released back to the wallet balance. You can safely create a new withdrawal. --- ## Webhook Verification Each webhook request includes a signature in the `x-webhook-signature` header. You should verify this signature to ensure the webhook came from Fystack. ### Headers - `x-webhook-signature` (string): Ed25519 signature of the canonicalized JSON payload. - `x-webhook-event` (string): The type of event that triggered this webhook. ### Webhook Payload - `webhook_id` (string): The UUID of the webhook configuration. - `resource_id` (string): The UUID of the resource that triggered the webhook. - `url` (string): The URL where the webhook was sent. - `event` (string): The type of event (e.g., `deposit.confirmed`). - `payload` (object): The event-specific data payload. ### Idempotency Key > **Warning.** Deposit webhooks (`deposit.pending` and `deposit.confirmed`) may be delivered more than once for the same deposit, for example after a retry following a timeout or a temporary outage on your endpoint. Always use `idempotency_key` to deduplicate processing instead of `resource_id` or `id` alone. For deposit events, the `payload.idempotency_key` field uniquely identifies a single underlying deposit event. Because network conditions or endpoint downtime can cause Fystack to redeliver the same webhook, you should treat any two deliveries with the same `idempotency_key` as the same event and process them only once. - `idempotency_key` (string): A unique key identifying this deposit event. Store the keys you've already processed (e.g. in a database with a unique constraint) and skip processing if an incoming webhook's `idempotency_key` has already been seen. ### Verifying Signatures To verify webhook signatures, you'll need to: 1. Canonicalize the JSON payload (ensure consistent ordering of object keys) 2. Verify the Ed25519 signature using your public key > **Warning.** Always canonicalize the event payload before verification. Different JSON serializers might produce different string representations of the same data structure. ```typescript import * as ed from '@noble/ed25519' // Example public key from Fystack const FYSTACK_PUBLIC_KEY = '503d18d8375e60667a4cdb879e72c249b3dc054e7d6443c5ccd93aef7f6547fb' // Canonicalize JSON by sorting keys recursively function canonicalizeJSON(inputObject: Record): string { if (typeof inputObject !== 'object' || inputObject === null) { throw new Error('Input must be a non-null object.') } const sortKeys = (value: any): any => { if (Array.isArray(value)) { return value.map(sortKeys) } if (value && typeof value === 'object' && value.constructor === Object) { return Object.keys(value) .sort() .reduce((sortedObj: Record, key: string) => { sortedObj[key] = sortKeys(value[key]) return sortedObj }, {}) } return value } return JSON.stringify(sortKeys(inputObject)) } // Verify Ed25519 signature async function verifyWebhook(payload: object, signatureHex: string): Promise { const canonical = canonicalizeJSON(payload) const msg = new TextEncoder().encode(canonical) return ed.verify(signatureHex, msg, FYSTACK_PUBLIC_KEY) } // Example usage app.post('/webhook', async (req, res) => { const signature = req.headers['x-webhook-signature'] const event = req.headers['x-webhook-event'] const payload = req.body // Verify signature using your stored public key const isValid = await verifyWebhook( payload, signature ) if (!isValid) { return res.status(400).json({ error: 'Invalid signature' }) } // Handle different event types switch (event) { case 'deposit.confirmed': // Handle confirmed deposit break case 'withdrawal.confirmed': // Handle confirmed withdrawal break // ... handle other events } res.json({ received: true }) }) ``` ```go package main import ( "crypto/ed25519" "encoding/hex" "encoding/json" "fmt" ) // Example public key from Fystack const FystackPublicKey = "503d18d8375e60667a4cdb879e72c249b3dc054e7d6443c5ccd93aef7f6547fb" // CanonicalizeJSON ensures consistent JSON representation func CanonicalizeJSON(data []byte) ([]byte, error) { var temp map[string]interface{} err := json.Unmarshal(data, &temp) if err != nil { return nil, fmt.Errorf("invalid JSON: %w", err) } canonicalJSON, err := json.Marshal(temp) if err != nil { return nil, fmt.Errorf("failed to marshal JSON: %w", err) } return canonicalJSON, nil } // VerifyWebhook verifies the webhook signature func VerifyWebhook(payload []byte, signatureHex string) (bool, error) { // Decode hex strings publicKey, err := hex.DecodeString(FystackPublicKey) if err != nil { return false, fmt.Errorf("invalid public key: %w", err) } signature, err := hex.DecodeString(signatureHex) if err != nil { return false, fmt.Errorf("invalid signature: %w", err) } // Canonicalize payload canonical, err := CanonicalizeJSON(payload) if err != nil { return false, fmt.Errorf("failed to canonicalize JSON: %w", err) } // Verify signature return ed25519.Verify(publicKey, canonical, signature), nil } // Example usage in HTTP handler func handleWebhook(w http.ResponseWriter, r *http.Request) { signature := r.Header.Get("x-webhook-signature") event := r.Header.Get("x-webhook-event") body, err := io.ReadAll(r.Body) if err != nil { http.Error(w, "Failed to read body", http.StatusBadRequest) return } isValid, err := VerifyWebhook( body, signature, ) if err != nil || !isValid { http.Error(w, "Invalid signature", http.StatusBadRequest) return } // Handle webhook... } ``` ```php getMessage()); return false; } } // Example usage in a webhook handler $signature = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? ''; $event = $_SERVER['HTTP_X_WEBHOOK_EVENT'] ?? ''; $payload = file_get_contents('php://input'); if (!verifyWebhook( $payload, $signature )) { http_response_code(400); echo json_encode(['error' => 'Invalid signature']); exit; } // Handle different event types switch ($event) { case 'deposit.confirmed': // Handle confirmed deposit break; case 'withdrawal.confirmed': // Handle confirmed withdrawal break; } echo json_encode(['received' => true]); ``` ```python import json import ed25519 from typing import Dict, Any from binascii import unhexlify # Example public key from Fystack FYSTACK_PUBLIC_KEY = '503d18d8375e60667a4cdb879e72c249b3dc054e7d6443c5ccd93aef7f6547fb' def canonicalize_json(data: Dict[Any, Any]) -> str: """Ensure consistent JSON representation.""" return json.dumps(data, sort_keys=True, separators=(',', ':')) def verify_webhook(payload: Dict[Any, Any], signature_hex: str) -> bool: """Verify webhook signature.""" try: # Convert hex strings to bytes public_key = unhexlify(FYSTACK_PUBLIC_KEY) signature = unhexlify(signature_hex) # Canonicalize payload canonical = canonicalize_json(payload).encode('utf-8') # Create verifier verifier = ed25519.VerifyingKey(public_key) return verifier.verify(signature, canonical) except Exception as e: print(f"Verification failed: {e}") return False # Example Flask route @app.route('/webhook', methods=['POST']) def handle_webhook(): signature = request.headers.get('x-webhook-signature') event = request.headers.get('x-webhook-event') payload = request.json if not verify_webhook( payload, signature ): return jsonify({'error': 'Invalid signature'}), 400 # Handle webhook... return jsonify({'received': True}) ``` ```java import com.fasterxml.jackson.databind.ObjectMapper; import org.bouncycastle.crypto.signers.Ed25519Signer; import org.bouncycastle.util.encoders.Hex; public class WebhookVerifier { private static final ObjectMapper MAPPER = new ObjectMapper(); // Example public key from Fystack private static final String FYSTACK_PUBLIC_KEY = "503d18d8375e60667a4cdb879e72c249b3dc054e7d6443c5ccd93aef7f6547fb"; public static String canonicalizeJSON(Object data) throws JsonProcessingException { // Use Jackson's sorted map feature for consistent ordering return MAPPER.writer().writeValueAsString(data); } public static boolean verifyWebhook(Object payload, String signatureHex) { try { byte[] publicKey = Hex.decode(FYSTACK_PUBLIC_KEY); byte[] signature = Hex.decode(signatureHex); // Canonicalize payload String canonical = canonicalizeJSON(payload); byte[] message = canonical.getBytes(StandardCharsets.UTF_8); Ed25519Signer verifier = new Ed25519Signer(); verifier.init(false, new Ed25519PublicKeyParameters(publicKey, 0)); verifier.update(message, 0, message.length); return verifier.verifySignature(signature); } catch (Exception e) { e.printStackTrace(); return false; } } // Example Spring controller @PostMapping("/webhook") public ResponseEntity handleWebhook( @RequestHeader("x-webhook-signature") String signature, @RequestHeader("x-webhook-event") String event, @RequestBody Map payload ) { if (!verifyWebhook( payload, signature )) { return ResponseEntity.badRequest().body("Invalid signature"); } // Handle webhook... return ResponseEntity.ok().build(); } } ``` > **Info.** The canonicalization step is crucial - it ensures that the same JSON data will always produce the same string representation, regardless of how it was originally formatted or ordered. ### Example Event Payloads **Event Examples** ```json { "webhook_id": "73893532-8e3d-4bc7-b3f9-148c9b82b811", "resource_id": "62ef8383-e897-449f-b9d8-78fffaa26a61", "url": "https://webhook.site/56e92433-823d-4b28-9cc9-9298076af671", "payload": { "amount": "1000", "asset": { "address": "0xDD75BEb521f4513D65fFbbE91ab7131f3cD92080", "address_type": "evm", "created_at": "2024-04-12T22:58:36.133934+07:00", "decimals": 6, "id": "f1be472d-1338-43ff-b547-06faba7ecce7", "is_native": false, "is_whitelisted": true, "logo_url": "https://assets-cdn.trustwallet.com/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png", "name": "USDC Sepolia test", "network_id": "23d13c03-1f3c-4e2a-9aef-1771a244b7f6", "symbol": "USDC", "updated_at": "2024-04-12T22:58:36.133934+07:00" }, "asset_hold": null, "asset_hold_id": null, "asset_id": "f1be472d-1338-43ff-b547-06faba7ecce7", "block_number": 8960514, "created_at": "2025-08-11T17:31:24+07:00", "description": "", "direction": "in", "from_address": "0xe6EBF81E9C225BbCEa9b5399E0D0d0f29f30f119", "id": "62ef8383-e897-449f-b9d8-78fffaa26a61", "idempotency_key": "bfa74b5d5b591794031e0f78a556c80b7b89875378baac4df75b8302cf80b72c", "method": "transfer", "network": null, "network_id": "23d13c03-1f3c-4e2a-9aef-1771a244b7f6", "price_native_token": "4260.480000022453", "price_token": "0.9997516371944777", "status": "pending", "to_address": "0xA118fa3d039BA6ad3BB0bA8ac8F243F727B2F99F", "tx_fee": "0.000051521446618", "tx_hash": "0x21c201ff8e02abdcd4395c71639266d953cb18cb159bdcd28b7556c482b4881a", "type": "token_transfer", "updated_at": "2025-08-11T17:31:26.815540431+07:00", "user": null, "user_id": "075243c1-962e-4803-83da-d73c78b8b060", "wallet": { "created_at": "2025-08-11T10:35:54.174577+07:00", "deleted_at": null, "disabled": false, "id": "e0851607-417a-475a-b399-5f3aa262b81d", "name": "hello1", "threshold": 1, "updated_at": "2025-08-11T10:35:54.174577+07:00", "user_id": "075243c1-962e-4803-83da-d73c78b8b060", "wallet_purpose": "general", "wallet_type": "standard", "workspace_id": "6bcfe6c7-281b-4773-a188-e78f5cad3336" }, "wallet_id": "e0851607-417a-475a-b399-5f3aa262b81d", "workspace": null, "workspace_id": "6bcfe6c7-281b-4773-a188-e78f5cad3336" }, "event": "deposit.pending" } ``` ```json { "webhook_id": "73893532-8e3d-4bc7-b3f9-148c9b82b811", "resource_id": "c6702df0-b8ef-4a38-882d-dabf0d56127d", "url": "https://webhook.site/56e92433-823d-4b28-9cc9-9298076af671", "event": "deposit.confirmed", "payload": { "id": "c6702df0-b8ef-4a38-882d-dabf0d56127d", "idempotency_key": "a1c92e7f4d3b8619524fbd0b3e9c15a6ed172b8d31f645a998e2c07b1a5f6d3e", "amount": "1000", "status": "confirmed", "tx_hash": "0x75789cbbe4d55328e6be7b7155b4028ea8afa384ffe13a1146d8bdb3eed6ba22", "from_address": "0xe6EBF81E9C225BbCEa9b5399E0D0d0f29f30f119", "to_address": "0xA793457d84CFD255Ed68295984300C4f799E759F", "asset_id": "f1be472d-1338-43ff-b547-06faba7ecce7", "wallet_id": "e0851607-417a-475a-b399-5f3aa262b81d", "block_number": 8960538, "type": "token_transfer", "direction": "in" } } ``` ```json { "webhook_id": "73893532-8e3d-4bc7-b3f9-148c9b82b811", "resource_id": "147d4808-2e98-4a86-9ead-c227e7e80cf6", "url": "https://webhook.site/56e92433-823d-4b28-9cc9-9298076af671", "event": "withdrawal.pending", "payload": { "id": "0de79230-a820-4bd4-b020-883d395faa87", "amount": "1000", "status": "PENDING_APPROVAL", "recipient_address": "0x7c2BbE2e02af09F1cf9aFd4A79EC201DC841675a", "asset_id": "f1be472d-1338-43ff-b547-06faba7ecce7", "wallet_id": "e0851607-417a-475a-b399-5f3aa262b81d", "creator_id": "075243c1-962e-4803-83da-d73c78b8b060", "asset_hold_id": "2595ac0b-deb6-4c58-8466-290a248e5d2f", "created_at": "2025-08-11T17:37:29.815148+07:00", "updated_at": "2025-08-11T17:37:29.815148+07:00" } } ``` ```json { "webhook_id": "73893532-8e3d-4bc7-b3f9-148c9b82b811", "resource_id": "0de79230-a820-4bd4-b020-883d395faa87", "url": "https://webhook.site/56e92433-823d-4b28-9cc9-9298076af671", "payload": { "amount": "1000", "asset": null, "asset_hold_id": "2595ac0b-deb6-4c58-8466-290a248e5d2f", "asset_id": "f1be472d-1338-43ff-b547-06faba7ecce7", "created_at": "2025-08-11T17:37:29.815148+07:00", "creator": null, "creator_id": "075243c1-962e-4803-83da-d73c78b8b060", "error_reason": null, "id": "0de79230-a820-4bd4-b020-883d395faa87", "recipient_address": "0x7c2BbE2e02af09F1cf9aFd4A79EC201DC841675a", "status": "EXECUTED", "transaction": { "amount": "1000", "asset": null, "asset_hold": null, "asset_hold_id": "2595ac0b-deb6-4c58-8466-290a248e5d2f", "asset_id": "f1be472d-1338-43ff-b547-06faba7ecce7", "block_number": 8960546, "created_at": "2025-08-11T17:37:45.828992+07:00", "description": "", "direction": "out", "from_address": "0xA118fa3d039BA6ad3BB0bA8ac8F243F727B2F99F", "id": "147d4808-2e98-4a86-9ead-c227e7e80cf6", "method": "transfer", "network": null, "network_id": "23d13c03-1f3c-4e2a-9aef-1771a244b7f6", "price_native_token": "4260.480000022453", "price_token": "0.9997516371944777", "status": "completed", "to_address": "0x7c2BbE2e02af09F1cf9aFd4A79EC201DC841675a", "tx_fee": "0.0000001851509817", "tx_hash": "0x619dea0c5eb5206a35ea22113d6b4d9e5f0ce226ae3b2ca0089ec9c341b3a210", "type": "withdrawal", "updated_at": "2025-08-11T17:37:50.765281+07:00", "user": null, "user_id": "075243c1-962e-4803-83da-d73c78b8b060", "wallet": null, "wallet_id": "e0851607-417a-475a-b399-5f3aa262b81d", "workspace": null, "workspace_id": "6bcfe6c7-281b-4773-a188-e78f5cad3336" }, "transaction_id": "147d4808-2e98-4a86-9ead-c227e7e80cf6", "updated_at": "2025-08-11T17:37:46.069818+07:00", "wallet_id": "e0851607-417a-475a-b399-5f3aa262b81d", "withdrawal_approvals": null }, "event": "withdrawal.executed" } ``` ```json { "webhook_id": "73893532-8e3d-4bc7-b3f9-148c9b82b811", "resource_id": "147d4808-2e98-4a86-9ead-c227e7e80cf6", "url": "https://webhook.site/56e92433-823d-4b28-9cc9-9298076af671", "payload": { "amount": "1000", "asset": null, "asset_hold_id": "2595ac0b-deb6-4c58-8466-290a248e5d2f", "asset_id": "f1be472d-1338-43ff-b547-06faba7ecce7", "created_at": "2025-08-11T17:37:29.815148+07:00", "creator": null, "creator_id": "075243c1-962e-4803-83da-d73c78b8b060", "error_reason": null, "id": "0de79230-a820-4bd4-b020-883d395faa87", "recipient_address": "0x7c2BbE2e02af09F1cf9aFd4A79EC201DC841675a", "status": "SUCCESS", "transaction": { "amount": "1000", "asset": null, "asset_hold": null, "asset_hold_id": "2595ac0b-deb6-4c58-8466-290a248e5d2f", "asset_id": "f1be472d-1338-43ff-b547-06faba7ecce7", "block_number": 8960546, "created_at": "2025-08-11T17:37:45.828992+07:00", "description": "", "direction": "out", "from_address": "0xA118fa3d039BA6ad3BB0bA8ac8F243F727B2F99F", "id": "147d4808-2e98-4a86-9ead-c227e7e80cf6", "method": "transfer", "network": null, "network_id": "23d13c03-1f3c-4e2a-9aef-1771a244b7f6", "price_native_token": "4260.480000022453", "price_token": "0.9997516371944777", "status": "confirmed", "to_address": "0x7c2BbE2e02af09F1cf9aFd4A79EC201DC841675a", "tx_fee": "0.0000001851509817", "tx_hash": "0x619dea0c5eb5206a35ea22113d6b4d9e5f0ce226ae3b2ca0089ec9c341b3a210", "type": "withdrawal", "updated_at": "2025-08-11T17:38:05.607714+07:00", "user": null, "user_id": "075243c1-962e-4803-83da-d73c78b8b060", "wallet": null, "wallet_id": "e0851607-417a-475a-b399-5f3aa262b81d", "workspace": null, "workspace_id": "6bcfe6c7-281b-4773-a188-e78f5cad3336" }, "transaction_id": "147d4808-2e98-4a86-9ead-c227e7e80cf6", "updated_at": "2025-08-11T17:38:06.057452+07:00", "wallet_id": "e0851607-417a-475a-b399-5f3aa262b81d", "withdrawal_approvals": null }, "event": "withdrawal.confirmed" } ``` ```json { "webhook_id": "73893532-8e3d-4bc7-b3f9-148c9b82b811", "resource_id": "3e8a6d21-7f4c-4b19-a0d5-2c9e1b7f8a36", "url": "https://webhook.site/56e92433-823d-4b28-9cc9-9298076af671", "payload": { "id": "3e8a6d21-7f4c-4b19-a0d5-2c9e1b7f8a36", "amount": "1000", "status": "FAILED", "recipient_address": "0x7c2BbE2e02af09F1cf9aFd4A79EC201DC841675a", "asset": null, "asset_id": "f1be472d-1338-43ff-b547-06faba7ecce7", "asset_hold_id": "8f4b2a90-1c6d-4e57-b3a8-0d9e6f1c2b75", "wallet_id": "e0851607-417a-475a-b399-5f3aa262b81d", "creator": null, "creator_id": "075243c1-962e-4803-83da-d73c78b8b060", "error_reason": null, "fee_priority": "standard", "approval_threshold": 0, "withdrawal_approvals": null, "transaction_id": "d47c1e93-5a2b-4f80-9e6d-1b3a8c7f0e52", "transaction": { "amount": "1000", "asset": null, "asset_hold": null, "asset_hold_id": "8f4b2a90-1c6d-4e57-b3a8-0d9e6f1c2b75", "asset_id": "f1be472d-1338-43ff-b547-06faba7ecce7", "block_number": 0, "created_at": "2025-08-11T18:10:02.118254+07:00", "description": "", "direction": "out", "from_address": "0xA118fa3d039BA6ad3BB0bA8ac8F243F727B2F99F", "id": "d47c1e93-5a2b-4f80-9e6d-1b3a8c7f0e52", "method": "transfer", "network": null, "network_id": "23d13c03-1f3c-4e2a-9aef-1771a244b7f6", "failed_reason": "Signing timeout", "price_native_token": "0", "price_token": "0", "status": "failed", "to_address": "0x7c2BbE2e02af09F1cf9aFd4A79EC201DC841675a", "tx_fee": "0", "tx_hash": null, "type": "withdrawal", "updated_at": "2025-08-11T18:15:02.540917+07:00", "user": null, "user_id": "075243c1-962e-4803-83da-d73c78b8b060", "wallet": null, "wallet_id": "e0851607-417a-475a-b399-5f3aa262b81d", "workspace": null, "workspace_id": "6bcfe6c7-281b-4773-a188-e78f5cad3336" }, "created_at": "2025-08-11T18:09:58.774102+07:00", "updated_at": "2025-08-11T18:15:02.611385+07:00" }, "event": "withdrawal.failed" } ``` > **Info.** Register your webhook endpoints in the Fystack portal and select which events you want to receive. --- # Checkout The Checkout API provides a powerful way to create payment sessions, manage payment flows, and process transactions through Fystack's secure infrastructure. --- ## Create a checkout **Endpoint:** `POST /checkouts` To create a new checkout, send a POST request to the `/checkouts` endpoint with the required parameters. ### Required parameters - `price` (string): The payment amount as a string (e.g., "66"). - `currency` (string): Three-letter ISO currency code (e.g., USD, EUR). Currently only USD is supported. - `expiry_duration_seconds` (number): Number of seconds until the checkout session expires. Must be between 0 and 31536000. - `destination_wallet_id` (string): Wallet ID where the payment will be sent to. _Image: Copy Wallet ID_ ### Asset parameters Provide either `supported_assets` or `accepted_assets`. - `supported_assets` (array): List of supported cryptocurrency assets. Each item can be an asset UUID or an alias in the format "SYMBOL:CHAIN_ID" (e.g., "SOL:1399811149"). | Chain | Chain ID | Format Example | | --- | --- | --- | | Solana | 1399811149 | `SOL:1399811149` | | Ethereum | 1 | `ETH:1`, `USDC:1` | | BNB Chain | 56 | `BNB:56` | | Base | 8453 | `ETH:8453` | | Polygon | 137 | `POL:137` | - `accepted_assets` (array): List of supported cryptocurrency assets in human-readable form. Each item contains `asset` and `network`, for example asset `USDC` on network `ETHEREUM_MAINNET`. ### Optional parameters - `success_url` (string): URL to redirect customers to after successful payment. - `cancel_url` (string): URL to redirect customers to if they cancel the checkout process. - `description` (string): Description of what the customer is purchasing. - `product_id` (string): Unique identifier for the product being purchased. - `customer_id` (string): Link this checkout to an existing customer in your system. - `order_id` (string): Unique identifier for the order. - `enable_localization` (boolean): Whether to enable localization for the checkout page. Default is false. - `from_address` (string): Optional payer address. If provided, Fystack uses the single wallet payment flow. - `underpaid_cover_percent` (number): Allowed underpayment tolerance percentage. Must be between 0 and 100. ### Response - `id` (string): Unique identifier for the checkout session. - `checkout_url` (string): URL to the hosted checkout page. Redirect customers to this URL. - `status` (string): Status of the checkout session. Can be `Pending`, `Confirming`, `Success`, `Expired`, `Unresolved`, `Resolved`, or `Cancelled`. - `created_at` (timestamp): Time at which the checkout session was created. - `updated_at` (timestamp): Time at which the checkout session was last updated. - `currency` (string): Three-letter ISO currency code. - `price` (string): The payment amount as a string. - `product_id` (string): Unique identifier for the product being purchased. - `order_id` (string): Unique identifier for the order. - `workspace_id` (string): Identifier for the workspace this checkout belongs to. - `description` (string): Description of what the customer is purchasing. - `supported_assets` (array): List of cryptocurrency assets supported for this checkout. - `customer_id` (string): Identifier for the customer associated with this checkout. - `expiry_duration_seconds` (number): Number of seconds until the checkout session expires. - `enable_localization` (boolean): Whether localization is enabled for the checkout page. - `success_url` (string): URL to redirect customers to after successful payment. - `cancel_url` (string): URL to redirect customers to if they cancel the checkout process. - `outcome` (object|null): Information about the payment outcome, if available. - `from_address` (string|null): The address from which the payment was made, if available. **Request** `POST /checkouts` ```typescript import { PaymentService, Environment } from '@fystack/sdk' const paymentService = new PaymentService({ apiKey: apiCredentials.APIKey, environment: Environment.Production }) await paymentService.createCheckout({ price: '66', currency: 'USD', supported_assets: ['SOL:1399811149', 'USDC:1399811149', 'ETH:8453', 'ETH:1', 'USDC:1'], description: 'Access to premium features and exclusive content.', success_url: 'https://example.com/payment/success', cancel_url: 'https://example.com/payment/cancel', product_id: '4c57fae9-1a54-4a1b-9d94-00d5b4a3e1d2', customer_id: 'bc5bd50d-265e-4ea9-b312-bd8de5118683', order_id: 'bc5bd50d-265e-4ea9-b312-bd8de5118683', enable_localization: false, destination_wallet_id: 'ca6b5c4d-753f-4c2d-af18-520f66a42bd4', expiry_duration_seconds: 3600 }) ``` ```bash curl -X POST https://api.fystack.io/api/v1/checkouts \ -H "ACCESS-API-KEY: your_api_key" \ -H "Content-Type: application/json" \ -d '{ "price": "66", "currency": "USD", "supported_assets": ["SOL:1399811149", "USDC:1399811149", "ETH:8453"], "description": "Access to premium features and exclusive content.", "success_url": "https://example.com/payment/success", "cancel_url": "https://example.com/payment/cancel", "product_id": "4c57fae9-1a54-4a1b-9d94-00d5b4a3e1d2", "customer_id": "bc5bd50d-265e-4ea9-b312-bd8de5118683", "order_id": "order_123", "enable_localization": false, "destination_wallet_id": "ca6b5c4d-753f-4c2d-af18-520f66a42bd4", "expiry_duration_seconds": 3600 }' ``` ```json { "success": true, "message": "success", "code": 0, "data": { "id": "17976d0a-e4ff-44a6-a47e-c7e2bbab3db2", "created_at": "2025-05-17T12:39:27.308105515+07:00", "updated_at": "2025-05-17T12:39:27.308105515+07:00", "currency": "USD", "product_id": "4c57fae9-1a54-4a1b-9d94-00d5b4a3e1d2", "order_id": "bc5bd50d-265e-4ea9-b312-bd8de5118683", "workspace_id": "ffb8c9d1-0bd2-4c30-bf4f-2a8fe7710d51", "underpaid_cover_percent": 0, "description": "Access to premium features and exclusive content.", "price": "66", "supported_assets": [ { "id": "6f4b5818-967f-4132-a475-20fe766c6c0e", "created_at": "0001-01-01T00:00:00Z", "updated_at": "0001-01-01T00:00:00Z", "is_native": false, "is_whitelisted": false }, { "id": "c0b89aea-08b1-4a10-a754-17f3b133be24", "created_at": "0001-01-01T00:00:00Z", "updated_at": "0001-01-01T00:00:00Z", "is_native": false, "is_whitelisted": false } ], "customer_id": "bc5bd50d-265e-4ea9-b312-bd8de5118683", "expiry_duration_seconds": 3600, "enable_localization": false, "success_url": "https://example.com/payment/success", "cancel_url": "https://example.com/payment/cancel", "status": "Pending", "outcome": null, "from_address": null, "checkout_url": "https://app.fystack.io/checkout/17976d0a-e4ff-44a6-a47e-c7e2bbab3db2" } } ``` --- ## Retrieve a checkout **Endpoint:** `GET /checkouts/:id` Retrieve the details of a checkout by its ID. ### Response - `id` (string): Unique identifier for the checkout. - `status` (string): Status of the checkout. Can be `Pending`, `Confirming`, `Success`, `Expired`, `Unresolved`, `Resolved`, or `Cancelled`. - `price` (string): The payment amount as a string. - `currency` (string): Three-letter ISO currency code. - `created_at` (timestamp): Time at which the checkout was created. - `updated_at` (timestamp): Time at which the checkout was last updated. - `product_id` (string): Unique identifier for the product being purchased. - `order_id` (string): Unique identifier for the order. - `workspace_id` (string): Identifier for the workspace this checkout belongs to. - `description` (string): Description of what the customer is purchasing. - `supported_assets` (array): List of cryptocurrency assets supported for this checkout. - `customer_id` (string): Identifier for the customer associated with this checkout. - `expiry_duration_seconds` (number): Number of seconds until the checkout expires. - `success_url` (string): URL to redirect customers to after successful payment. - `cancel_url` (string): URL to redirect customers to if they cancel the checkout process. - `checkout_url` (string): URL to the hosted checkout page. - `outcome` (object|null): Information about the payment outcome, if available. - `from_address` (string|null): The address from which the payment was made, if available. **Request** `GET /checkouts/:id` ```typescript import { PaymentService, Environment } from '@fystack/sdk' const paymentService = new PaymentService({ apiKey: apiCredentials.APIKey, environment: Environment.Production }) const checkout = await paymentService.getCheckout('17976d0a-e4ff-44a6-a47e-c7e2bbab3db2') ``` ```bash curl -X GET https://api.fystack.io/api/v1/checkouts/17976d0a-e4ff-44a6-a47e-c7e2bbab3db2 ``` ```json { "success": true, "message": "success", "code": 0, "data": { "id": "2912168a-6bb6-4b5c-88bb-723706718e4a", "created_at": "2025-05-17T12:50:57.807741+07:00", "updated_at": "2025-05-17T12:50:57.807741+07:00", "currency": "USD", "product_id": "4c57fae9-1a54-4a1b-9d94-00d5b4a3e1d2", "order_id": "bc5bd50d-265e-4ea9-b312-bd8de5118683", "workspace_id": "ffb8c9d1-0bd2-4c30-bf4f-2a8fe7710d51", "underpaid_cover_percent": 0, "description": "Access to premium features and exclusive content.", "price": "66", "supported_assets": [ { "id": "e9337429-a741-4e92-afbb-c1b1fc1ad8a4", "created_at": "2024-09-21T17:48:12.718886+07:00", "updated_at": "2024-09-21T17:48:12.718886+07:00", "name": "USD Coin", "symbol": "USDC", "decimals": 6, "logo_url": "https://assets-cdn.trustwallet.com/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png", "address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", "asset_type": "erc20", "is_native": false, "address_type": "evm", "is_whitelisted": true, "network_id": "dd21bfb5-f809-4070-a789-18ec8abbee1b" }, { "id": "6f4b5818-967f-4132-a475-20fe766c6c0e", "created_at": "2024-12-05T20:32:42.14387+07:00", "updated_at": "2024-12-05T20:32:42.14387+07:00", "name": "Solana token", "symbol": "SOL", "decimals": 9, "logo_url": "https://upload.wikimedia.org/wikipedia/en/b/b9/Solana_logo.png", "is_native": true, "address_type": "sol", "is_whitelisted": true, "network_id": "8ae1f43f-d428-484d-8ad4-ee9ccc65a9ac" } ], "customer_id": "bc5bd50d-265e-4ea9-b312-bd8de5118683", "expiry_duration_seconds": 3600, "enable_localization": false, "success_url": "https://example.com/payment/success", "cancel_url": "https://example.com/payment/cancel", "status": "Pending", "outcome": null, "from_address": null, "checkout_url": "https://app.fystack.io/checkout/2912168a-6bb6-4b5c-88bb-723706718e4a" } } ``` --- ## Create a checkout payment **Endpoint:** `POST /checkouts/:id/payment` Create a payment for an existing checkout session by specifying which supported asset to use for payment. ### Required parameters - `pay_asset_id` (string): The ID of the asset to use for payment. Must be one of the supported assets in the checkout session. ### Optional parameters - `customer_email` (string): Email address of the customer making the payment. ### Response - `id` (string): Unique identifier for the payment. - `checkout_id` (string): ID of the associated checkout session. - `customer_email` (string|null): Email of the customer, if provided. - `pay_asset_id` (string): ID of the asset being used for payment. - `network_id` (string): ID of the blockchain network for the transaction. - `expired_at` (timestamp): Time at which the payment will expire. - `payer_address` (string|null): Address from which the payment was sent, if available. - `tx_hash` (string|null): Transaction hash, if available. - `converted_crypto_price` (string): The payment amount converted to the selected cryptocurrency. - `deposit_address` (string): Address where payment should be sent. - `deposit_qr` (string): URL to a QR code containing the deposit address. - `created_at` (timestamp): Time at which the payment was created. - `updated_at` (timestamp): Time at which the payment was last updated. **Request** `POST /checkouts/:id/payment` ```typescript import { PaymentService, Environment } from '@fystack/sdk' const paymentService = new PaymentService({ apiKey: apiCredentials.APIKey, environment: Environment.Production }) // First get the checkout to see available assets const checkout = await paymentService.getCheckout('2912168a-6bb6-4b5c-88bb-723706718e4a') // Then create a payment using one of the supported assets const payment = await paymentService.createCheckoutPayment(checkout.id, { pay_asset_id: checkout.supported_assets[0].id }) ``` ```bash curl -X POST https://api.fystack.io/api/v1/checkouts/2912168a-6bb6-4b5c-88bb-723706718e4a/payment \ -H "Content-Type: application/json" \ -d '{ "pay_asset_id": "e9337429-a741-4e92-afbb-c1b1fc1ad8a4" }' ``` ```json { "success": true, "message": "success", "code": 0, "data": { "id": "06d92c50-e6f1-4637-8c20-72cf7e7d7b8a", "created_at": "2025-05-17T12:50:57.925269212+07:00", "updated_at": "2025-05-17T12:50:57.925269212+07:00", "checkout_id": "2912168a-6bb6-4b5c-88bb-723706718e4a", "checkout": null, "customer_email": null, "pay_asset_id": "e9337429-a741-4e92-afbb-c1b1fc1ad8a4", "network_id": "dd21bfb5-f809-4070-a789-18ec8abbee1b", "network": null, "expired_at": "2025-05-17T13:50:57.853990519+07:00", "payer_address": null, "tx_hash": null, "converted_crypto_price": "66", "deposit_address": "0xAbE72E79729e7463cd0822700255eD03bc6BD37C", "deposit_qr": "https://quickchart.io/qr?size=300&margin=2&text=0xAbE72E79729e7463cd0822700255eD03bc6BD37C" } } ``` --- ## Retrieve a checkout payment **Endpoint:** `GET /checkouts/payment/:id` Retrieve detailed information about a checkout payment by its ID. ### Response - `id` (string): Unique identifier for the payment. - `checkout_id` (string): ID of the associated checkout session. - `checkout` (object): The complete checkout session object. - `customer_email` (string|null): Email of the customer, if provided. - `pay_asset_id` (string): ID of the asset being used for payment. - `pay_asset` (object): Details of the asset being used for payment. - `network_id` (string): ID of the blockchain network for the transaction. - `network` (object): Details of the blockchain network. - `expired_at` (timestamp): Time at which the payment will expire. - `payer_address` (string|null): Address from which the payment was sent, if available. - `tx_hash` (string|null): Transaction hash, if available. - `converted_crypto_price` (string): The payment amount converted to the selected cryptocurrency. - `deposit_address` (string): Address where payment should be sent. - `deposit_qr` (string): URL to a QR code containing the deposit address. - `created_at` (timestamp): Time at which the payment was created. - `updated_at` (timestamp): Time at which the payment was last updated. **Request** `GET /checkouts/payment/:id` ```typescript import { PaymentService, Environment } from '@fystack/sdk' const paymentService = new PaymentService({ apiKey: apiCredentials.APIKey, environment: Environment.Production }) const checkoutPayment = await paymentService.getCheckoutPayment('06d92c50-e6f1-4637-8c20-72cf7e7d7b8a') console.log('checkout payment', checkoutPayment) ``` ```bash curl -X GET https://api.fystack.io/api/v1/checkouts/payment/06d92c50-e6f1-4637-8c20-72cf7e7d7b8a ``` ```json { "success": true, "message": "success", "code": 0, "data": { "id": "06d92c50-e6f1-4637-8c20-72cf7e7d7b8a", "created_at": "2025-05-17T12:50:57.925269+07:00", "updated_at": "2025-05-17T12:50:57.925269+07:00", "checkout_id": "2912168a-6bb6-4b5c-88bb-723706718e4a", "checkout": { "id": "2912168a-6bb6-4b5c-88bb-723706718e4a", "created_at": "2025-05-17T12:50:57.807741+07:00", "updated_at": "2025-05-17T12:50:57.807741+07:00", "currency": "USD", "product_id": "4c57fae9-1a54-4a1b-9d94-00d5b4a3e1d2", "order_id": "bc5bd50d-265e-4ea9-b312-bd8de5118683", "workspace_id": "ffb8c9d1-0bd2-4c30-bf4f-2a8fe7710d51", "underpaid_cover_percent": 0, "description": "Access to premium features and exclusive content.", "price": "66", "supported_assets": [], "customer_id": "bc5bd50d-265e-4ea9-b312-bd8de5118683", "expiry_duration_seconds": 3600, "enable_localization": false, "success_url": "https://example.com/payment/success", "cancel_url": "https://example.com/payment/cancel", "status": "Pending", "outcome": null, "from_address": null }, "customer_email": null, "pay_asset_id": "e9337429-a741-4e92-afbb-c1b1fc1ad8a4", "pay_asset": { "id": "e9337429-a741-4e92-afbb-c1b1fc1ad8a4", "symbol": "USDC", "name": "USD Coin", "logo_url": "https://assets-cdn.trustwallet.com/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png" }, "network_id": "dd21bfb5-f809-4070-a789-18ec8abbee1b", "network": { "id": "dd21bfb5-f809-4070-a789-18ec8abbee1b", "name": "Ethereum", "logo_url": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcRyVyCtMa_rKwrl1y0hyWcqPda3eG0vssJBAX5xQznEPw&s" }, "expired_at": "2025-05-17T13:50:57.85399+07:00", "payer_address": null, "tx_hash": null, "converted_crypto_price": "66", "deposit_address": "0xAbE72E79729e7463cd0822700255eD03bc6BD37C", "deposit_qr": "https://quickchart.io/qr?size=300&margin=2&text=0xAbE72E79729e7463cd0822700255eD03bc6BD37C" } } ``` ## Checkout Webhooks Fystack's Checkout API uses webhooks to notify your application when important events occur, such as when a checkout session is completed or a payment succeeds or fails. ### Common webhook events - `payment.received`: Triggered when a payment is received. - `payment.success`: Triggered when a payment is successful. ### Verifying webhook events For security reasons, you should always verify webhook events to ensure they were actually sent by Fystack. Each webhook request includes a signature in the `X-Webhook-Signature` header that you can use to verify the authenticity of the webhook payload. The event type is also included in the `X-Webhook-Event` header. To verify a webhook event, you'll need: 1. The webhook event payload 2. The signature from the `X-Webhook-Signature` header Using the SDK's `verifyEvent` method, you can easily check if the webhook is valid before processing it. ### Event payload structure - `webhook_id` (string): Unique identifier for the webhook configuration. - `resource_id` (string): Identifier for the resource associated with the event. - `url` (string): The URL where the webhook was sent. - `payload` (object): The actual event data, containing details about the checkout or payment. - `event` (string): The type of event (e.g., "payment.received", "payment.success"). **Verifying a webhook event** ```typescript import { APIService, Environment } from '@fystack/sdk' // Express.js example app.post('/webhook', express.raw({type: 'application/json'}), async (req, res) => { const signature = req.headers['x-webhook-signature'] as string; const eventType = req.headers['x-webhook-event'] as string; const event = req.body; const apiService = new APIService(apiCredentials, Environment.Production); try { const isValid = await apiService.Webhook.verifyEvent(event, signature); if (isValid) { // Process the webhook event if (eventType === 'payment.received') { const { checkout_id, tx_hash, status } = event.payload; console.log(`Payment received for checkout ${checkout_id}: ${tx_hash}`); // Update your database, notify your user, etc. } res.status(200).send('Webhook received and verified'); } else { console.warn('Invalid webhook signature'); res.status(400).send('Invalid signature'); } } catch (error) { console.error('Webhook verification failed', error); res.status(400).send('Webhook verification failed'); } }); ``` ```javascript import { APIService, Environment } from '@fystack/sdk' // Example of manually verifying a webhook const apiService = new APIService(apiCredentials, Environment.Production) const event = { webhook_id: 'add3a1be-fc91-413c-9b4a-028f0011954a', resource_id: 'checkout:b065623b-dc9c-4399-964a-99850d78d289:Pending', url: 'https://webhook.site/3a782a92-0d1b-4848-8c57-cc0837cba513', payload: { amount: '10', chain_id: 11155111, checkout_id: '664f5839-2b5a-4b8b-8507-3b9b4815b9c1', checkout_payment_id: 'b2f7d8b6-4e90-43b4-be6d-a5910ef53fdd', deposit_address: '0x06A388F07F0EB0605D57947A7F1856a5e091940b', from_address: 'ACCLHbGJYGy1vSL6RjHvaraT3XZn1bXMxz5fPVRA112G', network_code: 'ETHER_SEPOLIA_TESTNET', network_id: '23d13c03-1f3c-4e2a-9aef-1771a244b7f6', status: 'Pending', timestamp: '2025-02-09T23:42:24.83521+07:00', tx_hash: '0x928af7fee293a501fbbe847025a19d2f42ceaf4cd000112fbba63e053c6d02ea' }, event: 'payment.received' } const signature = '735d2bdca20d3414eb2dba1e2de54a262ca11e9fc80f6c0a05b5196fec18cce3' const isValid = await apiService.Webhook.verifyEvent(event, signature) if (isValid) { console.log('Webhook verified successfully') // Process the webhook } else { console.warn('Invalid webhook signature') } ``` ### Example webhook payload --- # Networks Networks represent the blockchain networks supported by Fystack. You can retrieve information about available networks, including EVM and non-EVM chains, along with their configuration details and health status. ## Supported networks The table below lists the mainnet networks currently supported by Fystack along with their `network_code` identifier. Use the `network_code` value when referencing a network in API requests, webhooks, or SDK calls. | Network | Network code | | --- | --- | | Ethereum | `ETHEREUM_MAINNET` | | Base | `BASE_MAINNET` | | Solana | `SOL_MAINNET` | | Polygon | `POLYGON_MAINNET` | | BNB Smart Chain | `BSC_MAINNET` | | Lisk | `LISK_MAINNET` | | Bitcoin | `BTC_MAINNET` | | Tron | `TRON_MAINNET` | > **Info.** Need a network that is not listed here? Reach out to the Fystack team and we can prioritize adding it. --- ## Network information Each network in Fystack contains comprehensive information about the blockchain: ### Network properties - **Chain identification**: Unique chain ID and internal code for each network - **Network type**: Support for both EVM and non-EVM networks - **Native currency**: The native token for each network (e.g., ETH, SOL) - **Block explorers**: Pre-configured explorer URLs for transactions, addresses, and tokens - **Network status**: Real-time RPC health status and availability - **Block configuration**: Confirmation requirements and block interval timing - **Testnet support**: Clearly identified test networks for development --- ## Get networks **Endpoint:** `GET /networks` This endpoint allows you to retrieve available blockchain networks. This is a public endpoint that does not require authentication. ### Optional parameters - `search` (string): Filter networks by name. For example, `?search=Base` will return networks matching "Base". ### Response - `success` (boolean): Indicates whether the request was successful. - `message` (string): A message describing the result of the operation. - `code` (integer): A numeric code indicating the result status (0 for success). - `data` (array): An array of network objects. - `id` (string): The UUID of the network. - `created_at` (string): Timestamp when the network was added to the system. - `updated_at` (string): Timestamp of the last network configuration update. - `name` (string): The display name of the network. - `description` (string): A brief description of the network. - `is_evm` (boolean): Whether this is an Ethereum Virtual Machine compatible network. - `chain_id` (integer): The unique chain identifier for the network. - `native_currency` (string): The symbol of the network's native currency. - `is_testnet` (boolean): Whether this is a test network. - `internal_code` (string): Internal identifier code for the network. - `explorer_tx` (string): URL template for transaction explorer links. Use `%s` as placeholder for transaction hash. - `explorer_address` (string): URL template for address explorer links. Use `%s` as placeholder for address. - `explorer_token` (string): URL template for token explorer links. Use `%s` as placeholder for token address. - `confirmation_blocks` (integer): Number of block confirmations required to consider a transaction final. - `block_interval_in_seconds` (integer): Average time between blocks in seconds. - `disabled` (boolean): Whether the network is currently disabled. - `logo_url` (string): URL to the network's logo image. - `is_rpc_active` (boolean): Whether the RPC endpoint is currently active. - `rpc_health_status` (string): Current health status of the RPC endpoint. Can be `healthy`, `degraded`, or `null`. - `pending_block_count` (integer): Number of pending blocks being processed. > **Info.** This endpoint is public and does not require authentication. You can use it to retrieve network information without API credentials. **Request** `GET /networks` ```bash curl -X GET https://api.fystack.io/api/v1/networks ``` ```bash curl -X GET "https://api.fystack.io/api/v1/networks?search=Base" ``` ```json { "success": true, "message": "success", "code": 0, "data": [ { "id": "23d13c03-1f3c-4e2a-9aef-1771a244b7f6", "created_at": "2024-01-05T09:05:25.629202+07:00", "updated_at": "2025-07-18T12:34:07.91766+07:00", "name": "Sepolia Ethereum Testnet", "description": "Sepolia testnet", "is_evm": true, "chain_id": 11155111, "native_currency": "ETH", "is_testnet": true, "internal_code": "ETHER_SEPOLIA_TESTNET", "explorer_tx": "https://sepolia.etherscan.io/tx/%s", "explorer_address": "https://sepolia.etherscan.io/address/%s", "explorer_token": "https://sepolia.etherscan.io/token/%s", "confirmation_blocks": 1, "block_interval_in_seconds": 15, "disabled": false, "logo_url": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcR-lzp6jMVVHdxHiK4q5Xy2FbJIrC-GPmTREA&s", "is_rpc_active": true, "rpc_health_status": "healthy", "pending_block_count": 8 }, { "id": "f8d7d8b9-3466-405c-a8c9-542366f95f39", "created_at": "2025-03-01T22:21:44.604716+07:00", "updated_at": "2025-08-03T21:47:21.917315+07:00", "name": "Base mainnet", "description": "Base mainnets", "is_evm": true, "chain_id": 8453, "native_currency": "ETH", "internal_code": "BASE_MAINNET", "explorer_tx": "https://basescan.org/tx/%s", "explorer_address": "https://basescan.org/address/%s", "explorer_token": "https://basescan.org/token/%s", "confirmation_blocks": 20, "block_interval_in_seconds": 2, "disabled": false, "logo_url": "https://avatars.githubusercontent.com/u/108554348?v=4", "is_rpc_active": false, "rpc_health_status": null, "pending_block_count": 25438 } ] } ``` --- ## Rescan transaction **Endpoint:** `POST /networks/rescan-transaction` This endpoint allows you to manually trigger a rescan of a specific transaction on a network. This is useful for cases where a transaction is not indexed after a certain time due to RPC outages. You can manually trigger a rescan so the system can index the transaction. > **Warning.** This endpoint requires a user access token and system admin permissions. ### Required parameters - `tx_hash` (string): The transaction hash to rescan. - `network_id` (string): The UUID of the network where the transaction occurred. ### Response - `success` (boolean): Indicates whether the rescan request was successful. - `message` (string): A message describing the result of the operation. - `code` (integer): A numeric code indicating the result status (0 for success). **Request** `POST /networks/rescan-transaction` ```typescript import { FystackSDK, Environment } from "@fystack/sdk" const apiCredentials = { apiKey: process.env.API_KEY, apiSecret: process.env.API_SECRET } const sdk = new FystackSDK({ credentials: apiCredentials, environment: Environment.Sandbox, logger: true }) await sdk.rescanTransaction({ txHash: 'a6ec569aa7db70433e9012eb456cd24156d33c1748825f0d868d2d4e2595cb06', networkId: 'c5725221-fa98-4d6c-a6ca-8c5cd65cd884' }) ``` ```bash curl -X POST https://api.fystack.io/api/v1/networks/rescan-transaction \ -H "Authorization: Bearer your_access_token" \ -H "Content-Type: application/json" \ -d '{ "tx_hash": "a6ec569aa7db70433e9012eb456cd24156d33c1748825f0d868d2d4e2595cb06", "network_id": "c5725221-fa98-4d6c-a6ca-8c5cd65cd884" }' ``` ```json { "success": true, "message": "success", "code": 0, "data": { "message": "Transaction block added to rescan queue successfully" } } ``` --- # Assets Assets represent the cryptocurrency tokens and coins supported by Fystack across different blockchain networks. You can query assets by various filters including network, symbol, and whitelist status. ## Asset information Each asset in Fystack contains comprehensive information about the token: ### Asset properties - **Token identification**: Unique ID, name, symbol, and contract address - **Network association**: The blockchain network where the asset exists - **Asset type**: Token standard (ERC20, TRC20, native, etc.) - **Whitelist status**: Whether the asset is approved for use on the platform - **Pricing**: Current USD price with last update timestamp - **Visual**: Logo URL for display purposes --- ## Get assets **Endpoint:** `GET /assets` This endpoint allows you to retrieve cryptocurrency assets across all supported networks. This is a public endpoint that does not require authentication. ### Optional parameters - `network_id` (string (uuid)): Filter assets by network UUID. For example, `?network_id=c03b242f-5091-499c-86c5-78dbc47ed719` will return assets only on BNB Mainnet. - `search` (string): Search assets by name or symbol. For example, `?search=USDC` will return assets matching "USDC". - `is_native` (boolean): Filter by native assets. Set to `true` to get only native currencies (ETH, BNB, etc.), or `false` to get only tokens. - `is_whitelisted` (boolean): Filter by whitelist status. Set to `true` to get only whitelisted assets approved for platform use. - `offset` (integer): The number of items to skip before starting to collect the result set. Maximum: 100000. - `limit` (integer): Limit the number of items returned per page. Maximum: 1000. ### Response - `success` (boolean): Indicates whether the request was successful. - `message` (string): A message describing the result of the operation. - `code` (integer): A numeric code indicating the result status (0 for success). - `data` (array): An array of asset objects. - `id` (string): The UUID of the asset. - `created_at` (string): Timestamp when the asset was added to the system. - `updated_at` (string): Timestamp of the last asset update. - `name` (string): The full display name of the asset. - `symbol` (string): The ticker symbol of the asset (e.g., USDC, ETH). - `decimals` (integer): The number of decimal places for the asset. - `logo_url` (string): URL to the asset's logo image. - `address` (string): The contract address of the asset. Empty for native currencies. - `asset_type` (string): The token standard or type. Can be `erc20`, `trc20`, `native`, etc. - `is_native` (boolean): Whether this is the native currency of the network. - `address_type` (string): The address format type. Can be `evm`, `tron`, etc. - `is_whitelisted` (boolean): Whether the asset is whitelisted and approved for platform use. - `network_id` (string): The UUID of the network this asset belongs to. - `network` (object): The full network object containing network details. - `network.id` (string): The UUID of the network. - `network.name` (string): The display name of the network. - `network.chain_id` (integer): The unique chain identifier for the network. - `network.native_currency` (string): The native currency symbol of the network. - `network.is_evm` (boolean): Whether the network is EVM compatible. - `network.explorer_tx` (string): URL template for transaction explorer links. - `network.explorer_address` (string): URL template for address explorer links. - `network.explorer_token` (string): URL template for token explorer links. - `price_usd` (string): The current price of the asset in USD. - `price_usd_updated_at` (string): Timestamp when the USD price was last updated. > **Info.** This endpoint is public and does not require authentication. You can use it to retrieve asset information without API credentials. **Request** `GET /assets` ```bash curl -X GET "https://api.fystack.io/api/v1/assets?search=USDC" ``` ```json { "success": true, "message": "success", "code": 0, "data": [ { "id": "bf130274-e80c-43f3-aac3-c7dbfcbd74cd", "created_at": "2026-01-21T12:27:39.902081+07:00", "updated_at": "2026-01-21T12:27:39.902081+07:00", "name": "USDC (Binance-Peg USD Coin)", "symbol": "USDC", "decimals": 18, "logo_url": "https://bscscan.com/token/images/centre-usdc_28.png", "address": "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d", "asset_type": "erc20", "is_native": false, "address_type": "evm", "is_whitelisted": true, "network_id": "c03b242f-5091-499c-86c5-78dbc47ed719", "network": { "id": "c03b242f-5091-499c-86c5-78dbc47ed719", "created_at": "2024-11-03T20:18:36.254727+07:00", "updated_at": "2025-08-22T08:32:32.534204+07:00", "name": "BNB Mainnet", "description": "BNB Mainnet", "is_evm": true, "chain_id": 56, "native_currency": "BNB", "internal_code": "BSC_MAINNET", "explorer_tx": "https://bscscan.com/tx/%s", "explorer_address": "https://bscscan.com/address/%s", "explorer_token": "https://bscscan.com/token/%s", "confirmation_blocks": 12, "block_interval_in_seconds": 3.2, "disabled": false, "logo_url": "https://s2.coinmarketcap.com/static/img/coins/64x64/1839.png" }, "price_usd": "1", "price_usd_updated_at": "2026-02-03T06:47:55.879075+07:00" }, { "id": "a52495a7-39b3-4ee9-9a94-e461e2057748", "created_at": "2026-01-09T11:09:23.092265+07:00", "updated_at": "2026-01-09T11:15:12.374079+07:00", "name": "Bridged USDC on Lisk", "symbol": "USDC", "decimals": 6, "logo_url": "https://assets-cdn.trustwallet.com/blockchains/base/assets/0xEB466342C4d449BC9f53A865D5Cb90586f405215/logo.png", "address": "0xF242275d3a6527d877f2c927a82D9b057609cc71", "asset_type": "erc20", "is_native": false, "address_type": "evm", "is_whitelisted": true, "network_id": "d2d90216-2530-4704-bc58-48719dacb76f", "network": { "id": "d2d90216-2530-4704-bc58-48719dacb76f", "created_at": "2026-01-08T11:58:34.277954+07:00", "updated_at": "2026-01-08T11:58:34.277954+07:00", "name": "Lisk", "description": "Lisk is an Ethereum Layer-2 blockchain network.", "is_evm": true, "chain_id": 1135, "native_currency": "ETH", "is_testnet": true, "internal_code": "LISK_MAINNET", "explorer_tx": "https://blockscout.lisk.com/tx/%s", "explorer_address": "https://blockscout.lisk.com/address/%s", "explorer_token": "https://blockscout.lisk.com/token/%s", "confirmation_blocks": 5, "block_interval_in_seconds": 2, "disabled": false, "logo_url": "https://avatars.githubusercontent.com/u/16600915?s=200&v=4" }, "price_usd": "1", "price_usd_updated_at": "2026-02-03T06:47:55.879075+07:00" } ] } ``` --- ## Add asset to wallet **Endpoint:** `PUT /wallets/:wallet_id/asset/:asset_id` This endpoint adds an asset to a wallet, enabling the wallet to hold and receive that asset. If the asset was previously removed, it will be restored automatically. The wallet must already have an address type that supports the asset's network. For example, adding an ERC-20 token requires the wallet to have an EVM address. ### Path parameters - `wallet_id` (string (uuid)): The UUID of the wallet to add the asset to. - `asset_id` (string (uuid)): The UUID of the asset to add. ### Response - `success` (boolean): Indicates whether the request was successful. - `message` (string): A message describing the result of the operation. - `code` (integer): A numeric code indicating the result status (0 for success). - `data` (object): The wallet asset record. - `id` (string): The UUID of the wallet asset record. - `wallet_id` (string): The UUID of the wallet. - `asset_id` (string): The UUID of the asset. - `deposit_address` (string): The deposit address assigned for this asset on the wallet. > **Info.** If the asset already exists in the wallet, the existing record is returned. Re-adding a previously removed asset restores it without creating a duplicate. **Request** `PUT /wallets/:wallet_id/asset/:asset_id` ```bash curl -X PUT https://api.fystack.io/api/v1/wallets/c2b71329-34c9-45aa-89d3-ddde8b15dec5/asset/bf130274-e80c-43f3-aac3-c7dbfcbd74cd \ -H "ACCESS-API-KEY: your_api_key" \ -H "ACCESS-TIMESTAMP: 1667836889" \ -H "ACCESS-SIGN: YourBase64EncodedSignature==" ``` ```json { "success": true, "message": "success", "code": 0, "data": { "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "wallet_id": "c2b71329-34c9-45aa-89d3-ddde8b15dec5", "asset_id": "bf130274-e80c-43f3-aac3-c7dbfcbd74cd", "deposit_address": "0xA793457d84CFD255Ed68295984300C4f799E759F", "hidden": false } } ``` --- ## Remove asset from wallet **Endpoint:** `DELETE /wallets/:wallet_id/asset/:asset_id` This endpoint removes an asset from a wallet. The asset can only be removed if its balance is zero. If the asset still holds funds, you must transfer or withdraw them first. ### Path parameters - `wallet_id` (string (uuid)): The UUID of the wallet to remove the asset from. - `asset_id` (string (uuid)): The UUID of the asset to remove. ### Response - `success` (boolean): Indicates whether the request was successful. - `message` (string): A message describing the result of the operation. - `code` (integer): A numeric code indicating the result status (0 for success). > **Warning.** You cannot remove an asset that has a non-zero balance. Transfer or withdraw all funds before removing the asset. **Request** `DELETE /wallets/:wallet_id/asset/:asset_id` ```bash curl -X DELETE https://api.fystack.io/api/v1/wallets/c2b71329-34c9-45aa-89d3-ddde8b15dec5/asset/bf130274-e80c-43f3-aac3-c7dbfcbd74cd \ -H "ACCESS-API-KEY: your_api_key" \ -H "ACCESS-TIMESTAMP: 1667836889" \ -H "ACCESS-SIGN: YourBase64EncodedSignature==" ``` ```json { "success": true, "message": "success", "code": 0, "data": null } ``` --- ## Common use cases ### Finding stablecoins To find all stablecoins like USDC and USDT across networks: ```bash curl -X GET "https://api.fystack.io/api/v1/assets?search=USD&is_whitelisted=true" ``` ### Getting native currencies To get all native currencies (ETH, BNB, etc.) for gas estimation: ```bash curl -X GET "https://api.fystack.io/api/v1/assets?is_native=true" ``` ### Listing assets on a specific network To get all assets available on a specific network (e.g., BNB Mainnet): ```bash curl -X GET "https://api.fystack.io/api/v1/assets?network_id=c03b242f-5091-499c-86c5-78dbc47ed719&is_whitelisted=true" ``` ### Pagination For large result sets, use pagination to retrieve assets in batches: ```bash # First page curl -X GET "https://api.fystack.io/api/v1/assets?limit=50&offset=0" # Second page curl -X GET "https://api.fystack.io/api/v1/assets?limit=50&offset=50" ``` --- # Workspaces Workspaces provide a centralized view of your blockchain operations, including wallet statistics, transaction volumes, network fees, and smart contract activity. --- ## Get workspace statistics **Endpoint:** `GET /workspaces/{workspace_id}/stats` Retrieve comprehensive statistics and analytics for a specific workspace, including transaction volumes, network fees, wallet counts, and smart contract activity. ### Path parameters - `workspace_id` (string): The UUID of the workspace to retrieve statistics for. ### Response - `success` (boolean): Indicates whether the request was successful. - `message` (string): A message describing the result of the operation. - `code` (integer): A numeric code indicating the result status (0 for success). - `data` (object): Contains the workspace statistics. - `total_value` (string): Total value of all assets in the workspace (in USD). - `total_wallets` (integer): Total number of wallets in this workspace. - `total_smart_contracts` (integer): Total number of smart contracts deployed in this workspace. - `total_pending_transactions` (integer): Number of transactions currently pending. - `daily_volume_stats` (array): Array of daily transaction volume statistics for the past 8 days. - `id` (object): Date identifier for the daily stats. - `day` (integer): Day of the month. - `month` (integer): Month number (1-12). - `total` (string): Total transaction volume for the day (in USD). - `total_inflow_usd` (string): Total incoming transaction volume (in USD). - `total_outflow_usd` (string): Total outgoing transaction volume (in USD). - `monthly_stats` (array): Array of monthly transaction statistics. - `id` (object): Date identifier for the monthly stats. - `month` (integer): Month number (1-12). - `year` (integer): Year. - `total_transaction` (integer): Total number of transactions in the month. - `total_volume` (string): Total transaction volume for the month (in USD). - `monthly_total_fee_spent_per_network` (array): Array of network fees spent per month, broken down by blockchain network. - `id` (object): Identifier for the fee record. - `month` (integer): Month number (1-12). - `network_id` (string): UUID of the blockchain network. - `year` (integer): Year. - `total_fee_spent` (string): Total fees spent in the network's native currency. - `value` (string): Total fees spent in USD. - `network` (object): Network details. - `name` (string): Name of the blockchain network (e.g., "Ethereum", "Solana mainnet", "Base"). - `native_currency` (string): Native currency symbol for the network (e.g., "ETH", "SOL"). > **Info.** Daily volume stats show the last 8 days of transaction activity, while monthly stats provide a historical view of transaction volumes and counts by month. **Request** `GET /workspaces/{workspace_id}/stats` ```bash curl -X GET https://api.fystack.io/api/v1/workspaces/ffb8c9d1-0bd2-4c30-bf4f-2a8fe7710d51/stats \ -H "ACCESS-API-KEY: your_api_key" \ -H "ACCESS-TIMESTAMP: 1667836889" \ -H "ACCESS-SIGN: YourBase64EncodedSignature==" ``` ```json { "success": true, "message": "success", "code": 0, "data": { "total_value": "1534.84534320254792485502487275776", "total_wallets": 39, "total_smart_contracts": 0, "total_pending_transactions": 0, "daily_volume_stats": [ { "id": { "day": 16, "month": 11 }, "total": "0", "total_inflow_usd": "0", "total_outflow_usd": "0" }, { "id": { "day": 15, "month": 11 }, "total": "0", "total_inflow_usd": "0", "total_outflow_usd": "0" }, { "id": { "day": 14, "month": 11 }, "total": "0", "total_inflow_usd": "0", "total_outflow_usd": "0" }, { "id": { "day": 13, "month": 11 }, "total": "0", "total_inflow_usd": "0", "total_outflow_usd": "0" }, { "id": { "day": 12, "month": 11 }, "total": "0", "total_inflow_usd": "0", "total_outflow_usd": "0" }, { "id": { "day": 11, "month": 11 }, "total": "0", "total_inflow_usd": "0", "total_outflow_usd": "0" }, { "id": { "day": 10, "month": 11 }, "total": "0", "total_inflow_usd": "0", "total_outflow_usd": "0" }, { "id": { "day": 9, "month": 11 }, "total": "0", "total_inflow_usd": "0", "total_outflow_usd": "0" } ], "monthly_stats": [ { "id": { "month": 2, "year": 2025 }, "total_transaction": 1, "total_volume": "3.0232548466894343" }, { "id": { "month": 3, "year": 2025 }, "total_transaction": 0, "total_volume": "0" }, { "id": { "month": 4, "year": 2025 }, "total_transaction": 2, "total_volume": "5.38689432482736497038665" }, { "id": { "month": 5, "year": 2025 }, "total_transaction": 2, "total_volume": "3.4820715177482234" }, { "id": { "month": 6, "year": 2025 }, "total_transaction": 3, "total_volume": "6016.123408531333541256" }, { "id": { "month": 7, "year": 2025 }, "total_transaction": 24, "total_volume": "716.62394897558719889991228" }, { "id": { "month": 8, "year": 2025 }, "total_transaction": 0, "total_volume": "0" }, { "id": { "month": 9, "year": 2025 }, "total_transaction": 19, "total_volume": "61.99795033878070467252234" }, { "id": { "month": 10, "year": 2025 }, "total_transaction": 2, "total_volume": "10.997992387864311" }, { "id": { "month": 11, "year": 2025 }, "total_transaction": 0, "total_volume": "0" } ], "monthly_total_fee_spent_per_network": [ { "id": { "month": 7, "network_id": "ab3553b6-1472-4ee2-bbb7-7a3df71db90d", "year": 2025 }, "total_fee_spent": "0.000000408265132", "value": "0.0012847633534240739867953232", "network": { "name": "Base", "native_currency": "ETH" } }, { "id": { "month": 7, "network_id": "8ae1f43f-d428-484d-8ad4-ee9ccc65a9ac", "year": 2025 }, "total_fee_spent": "0.00004", "value": "0.0055775034112016304", "network": { "name": "Solana mainnet", "native_currency": "SOL" } }, { "id": { "month": 6, "network_id": "8ae1f43f-d428-484d-8ad4-ee9ccc65a9ac", "year": 2025 }, "total_fee_spent": "0.000005", "value": "0.0006971879264002038", "network": { "name": "Solana mainnet", "native_currency": "SOL" } }, { "id": { "month": 4, "network_id": "8ae1f43f-d428-484d-8ad4-ee9ccc65a9ac", "year": 2025 }, "total_fee_spent": "0.00001", "value": "0.0013943758528004076", "network": { "name": "Solana mainnet", "native_currency": "SOL" } }, { "id": { "month": 6, "network_id": "dd21bfb5-f809-4070-a789-18ec8abbee1b", "year": 2025 }, "total_fee_spent": "0.0001131949830806", "value": "0.35621157589674587496249666856", "network": { "name": "Ethereum", "native_currency": "ETH" } }, { "id": { "month": 2, "network_id": "dd21bfb5-f809-4070-a789-18ec8abbee1b", "year": 2025 }, "total_fee_spent": "0.0000294725852381", "value": "0.09274683159711570977946276556", "network": { "name": "Ethereum", "native_currency": "ETH" } }, { "id": { "month": 9, "network_id": "8ae1f43f-d428-484d-8ad4-ee9ccc65a9ac", "year": 2025 }, "total_fee_spent": "0.00001", "value": "0.0013943758528004076", "network": { "name": "Solana mainnet", "native_currency": "SOL" } }, { "id": { "month": 5, "network_id": "8ae1f43f-d428-484d-8ad4-ee9ccc65a9ac", "year": 2025 }, "total_fee_spent": "0.00001", "value": "0.0013943758528004076", "network": { "name": "Solana mainnet", "native_currency": "SOL" } }, { "id": { "month": 6, "network_id": "ab3553b6-1472-4ee2-bbb7-7a3df71db90d", "year": 2025 }, "total_fee_spent": "0.000000012727953", "value": "0.0000400534023035403443967228", "network": { "name": "Base", "native_currency": "ETH" } } ] } } ``` --- ## Get workspace balances by symbol **Endpoint:** `GET /workspaces/{workspace_id}/balances` Query workspace balances filtered by one or more asset symbols. This endpoint aggregates balances across all networks for the specified symbols, providing a consolidated view of asset holdings. ### Path parameters - `workspace_id` (string): The UUID of the workspace to retrieve balances for. ### Query parameters - `symbol` (string): Asset symbol to filter by (e.g., USDC, SOL, ETH). Can be specified multiple times to query multiple symbols: `?symbol=USDC&symbol=SOL&symbol=ETH` ### Response - `success` (boolean): Indicates whether the request was successful. - `message` (string): A message describing the result of the operation. - `code` (integer): A numeric code indicating the result status (0 for success). - `data` (array): Array of balance information grouped by symbol. - `symbol` (string): The asset symbol (e.g., "USDC", "SOL", "ETH"). - `total_balance` (string): Total balance of the asset across all networks. - `total_value_usd` (string): Total value of the asset in USD. - `total_wallet_count` (integer): Total number of wallets holding this asset. - `networks` (array): Array of network-specific balance information. - `asset_id` (string): UUID of the asset. - `asset_name` (string): Name of the asset (e.g., "USDC Sepolia test"). - `asset_logo` (string): URL to the asset logo image. - `network_id` (string): UUID of the blockchain network. - `network_name` (string): Name of the blockchain network (e.g., "Sepolia Ethereum Testnet", "Solana devnet"). - `network_logo` (string): URL to the network logo image. - `balance` (string): Balance of the asset on this specific network. - `wallet_count` (integer): Number of wallets holding this asset on this network. > **Info.** You can query multiple symbols in a single request by repeating the `symbol` parameter. The response groups balances by symbol and shows the distribution across different networks. **Request** `GET /workspaces/{workspace_id}/balances` ```bash curl -X GET "https://api.fystack.io/api/v1/workspaces/ffb8c9d1-0bd2-4c30-bf4f-2a8fe7710d51/balances?symbol=USDC&symbol=SOL" \ -H "ACCESS-API-KEY: your_api_key" \ -H "ACCESS-TIMESTAMP: 1667836889" \ -H "ACCESS-SIGN: YourBase64EncodedSignature==" ``` ```json { "success": true, "message": "success", "code": 0, "data": [ { "symbol": "SOL", "total_balance": "0.22379716", "total_value_usd": "29.8080102529293016511548", "total_wallet_count": 7, "networks": [ { "asset_id": "d10f85c1-a89d-439f-b488-57cd06b064d5", "asset_name": "Solana dev net", "asset_logo": "https://upload.wikimedia.org/wikipedia/en/b/b9/Solana_logo.png", "network_id": "2e89cd02-8585-47b3-90e3-a92185f6ca5f", "network_name": "Solana devnet", "network_logo": "https://upload.wikimedia.org/wikipedia/en/b/b9/Solana_logo.png", "balance": "0.22379716", "wallet_count": 7 } ] }, { "symbol": "USDC", "total_balance": "36515.008265", "total_value_usd": "36510.1973390962088334367595", "total_wallet_count": 25, "networks": [ { "asset_id": "f1be472d-1338-43ff-b547-06faba7ecce7", "asset_name": "USDC Sepolia test", "asset_logo": "https://assets-cdn.trustwallet.com/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png", "network_id": "23d13c03-1f3c-4e2a-9aef-1771a244b7f6", "network_name": "Sepolia Ethereum Testnet", "network_logo": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcR-lzp6jMVVHdxHiK4q5Xy2FbJIrC-GPmTREA&s", "balance": "33426.996114", "wallet_count": 16 }, { "asset_id": "af836c8e-3199-426d-bb00-17ae3a5c2f5a", "asset_name": "USDC-Test", "asset_logo": "https://s2.coinmarketcap.com/static/img/coins/64x64/3408.png", "network_id": "2e89cd02-8585-47b3-90e3-a92185f6ca5f", "network_name": "Solana devnet", "network_logo": "https://upload.wikimedia.org/wikipedia/en/b/b9/Solana_logo.png", "balance": "35.012151", "wallet_count": 5 }, { "asset_id": "60cd62ac-f100-42aa-a16e-d1a8ed28e39f", "asset_name": "USDC( Tron testnet)", "asset_logo": "https://assets-cdn.trustwallet.com/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png", "network_id": "c5725221-fa98-4d6c-a6ca-8c5cd65cd884", "network_name": "Tron testnet", "network_logo": "https://s2.coinmarketcap.com/static/img/coins/64x64/1958.png", "balance": "3053", "wallet_count": 4 } ] } ] } ``` --- # Policies Fystack policies are programmable guardrails for sensitive wallet actions. A policy targets a workspace, optionally narrows to specific wallets, and contains rules that evaluate an expression against a structured action payload. Rules run for four actions: - `withdrawal.create` (trigger_action): Evaluated before a withdrawal is created. A `DENY` blocks the request. A matching `ALLOW` bypasses transaction approval and lets the withdrawal move to signing. - `web3.transfer` (trigger_action): Evaluated when a native-coin or ERC-20 transfer is sent through the Web3 raw-transaction API. Uses the same `withdrawal.*` fields as `withdrawal.create`, so one condition reads the same on both triggers. Velocity fields are not available, because these transfers are not withdrawals. ERC-20 transfers are also evaluated as `web3.contract_call`. - `web3.contract_call` (trigger_action): Evaluated before EVM contract calls and ERC-20 token transfers are sent to signing. A `DENY` records the activity as `denied_by_policy`. A matching `ALLOW` skips transaction approval and proceeds to signing. - `web3.sign_message` (trigger_action): Evaluated when a wallet is asked to sign a message with `personal_sign`, `eth_sign`, or `eth_signTypedData_v4`. Deny only: a signed message always goes through approval. > **Info.** The policy schema is generated from the backend action schema in Apex. Use > [Get the policy schema](#get-the-policy-schema) when building UI builders or > validating field availability dynamically. For ready-made rules, see > [Policy Templates](https://docs.fystack.io/product/policies.md#policy-templates). --- ## Policy structure A policy is the container. A rule is the executable unit. ```json { "name": "Large withdrawal guard", "approval_group_id": "a1b2c3d4-e5f6-7890-1234-567890abcdef", "wallet_ids": [], "rules": [ { "trigger_action": "withdrawal.create", "condition": "withdrawal.value_usd > 10000 && !withdrawal.is_whitelisted", "effect": "DENY" } ] } ``` - `approval_group_id` (string): The approval group that reviews policy creation, policy edits, policy deletion, and rule changes. - `wallet_ids` (string\[]): Wallets this policy applies to. An empty array means every wallet in the workspace. - `rules` (array): Up to 50 rules. Each rule has a `trigger_action`, `condition`, `effect`, optional `description`, and optional `is_enabled`. ### Policy statuses - `pending` (status): Created but not approved yet. Pending policies are not evaluated. - `active` (status): Approved and live. Only active policies are loaded by the evaluator. - `inactive` (status): Disabled and not evaluated. - `rejected` (status): Rejected during approval and not evaluated. --- ## Policy evaluation For each guarded request, Fystack loads active policies for the workspace, compiles enabled rules, filters matches by action and wallet targeting, then resolves the result: 1. If any matching rule has `effect: "DENY"`, the action is blocked. 2. Otherwise, if any matching rule has `effect: "ALLOW"`, the action proceeds and transaction approval is bypassed. 3. Otherwise, no policy rule matched. The action continues through the normal approval flow. `DENY` always wins over `ALLOW`, so rule order never changes the outcome. Within the winning effect, the first match in declaration order is used for attribution, so the API can surface a stable `rule_id` and policy reason. There is no separate "require approval" effect. Approval is the default for any action that no `ALLOW` rule matches. > **Warning.** Evaluation is fail-closed. Cache, database, compile, or runtime evaluation > failures deny the action. Each evaluation has a 100 ms timeout. ### Scope and caching Policies are scoped by workspace. Apex caches a compiled policy bundle per workspace and invalidates it after policy approvals, rejections, and wallet targeting changes. Wallet filtering happens during evaluation, so one workspace bundle can serve all wallets in that workspace. --- ## Conditions `condition` is an expression that must return a boolean. Reference fields by section and field name, for example `principal.role`, `withdrawal.value_usd`, `wallet.purpose`, or `resource.method_selector`. Use normal boolean, comparison, and membership operators: ```txt && || ! == != > >= < <= in ``` `in` checks membership in a list literal or a list field, for example `principal.api_key_id in [""]` or `"0x…" in resource.permit_tokens`. Examples: - `Deny large unwhitelisted withdrawals` (DENY): `withdrawal.value_usd > 10000 && !withdrawal.is_whitelisted` - `Cap each wallet at $100,000 per day` (DENY): `wallet.projected_outflow_24h > 100000` - `Keep custody funds inside the workspace` (DENY): `wallet.purpose == "custody" && !withdrawal.destination_is_internal` - `Bypass approval for a payout API key` (ALLOW): `principal.initiator_type == "api_key" && principal.api_key_id in [""] && withdrawal.value_usd < 1000` - `Bypass approval for aged address book entries` (ALLOW): `withdrawal.is_whitelisted && withdrawal.address_age_sec >= 86400 && withdrawal.value_usd <= 1000` - `Deny weekend high-value withdrawals` (DENY): `(context.day_of_week == "Saturday" || context.day_of_week == "Sunday") && withdrawal.value_usd > 25000` - `Deny high-gas ERC-20 transfers` (DENY): `resource.method_selector == "0xa9059cbb" && resource.gas_limit > 100000` - `Deny unlimited token permits` (DENY): `resource.is_permit && resource.permit_amount_is_unlimited` > **Warning.** `resource.decoded_args` is dynamic. Guard dynamic fields with `has(...)` > before reading them, for example `has(resource.decoded_args.to) && > resource.decoded_args.to == "0x000000000000000000000000000000000000dead"`. ### Reference fields Fields marked with a `ref` in the schema hold the ID or code of another workspace entity: `workspace_user`, `api_key`, `wallet`, `address_book`, `network`, or `asset_symbol`. Condition builders should render a picker for these fields. When a rule is saved, Fystack checks that each referenced ID belongs to your workspace. If an entity is later deleted or revoked, the rule keeps the ID and can only match less, never more. --- ## Action schema The action schema defines the fields available inside a condition. Fields marked `deprecated` are still evaluated for existing rules but should not be used in new ones. ### Common fields Every trigger action exposes the following fields. - `principal.user_id` (string · ref workspace_user): ID of the workspace member who initiated the action. Empty when the initiator is an API key. - `principal.role` (string): Workspace role. One of `owner`, `admin`, `signer`, `proposer`, or `viewer`. - `principal.initiator_type` (string): `user` for a web session, or `api_key` for an API key. - `principal.api_key_id` (string · ref api_key): ID of the API key that initiated the action. Empty for web sessions, so combine it with `principal.initiator_type`. - `principal.user_email` (string · deprecated): Email of the initiating user. Use `principal.user_id` instead, because an email can change. - `wallet.id` (string · ref wallet): Source wallet ID. Use it for rules that target specific wallets inside one policy. - `wallet.purpose` (string): Source wallet purpose. One of `general`, `custody`, `payment`, `user`, `one_time_use`, `gas_tank`, or `deployment`. - `wallet.type` (string): Source wallet type. `standard` or `mpc`. - `context.hour` (integer): UTC hour of evaluation, `0` through `23`. - `context.day_of_week` (string): UTC weekday name, for example `Monday` or `Saturday`. ### Withdrawal fields Available on `withdrawal.create` and `web3.transfer`. - `withdrawal.amount` (number): Requested asset amount. - `withdrawal.value_usd` (number): Requested value in USD. - `withdrawal.asset_symbol` (string · ref asset_symbol): Asset symbol, such as `USDT`, `ETH`, or `BTC`. - `withdrawal.asset_is_native` (boolean): Whether the asset is the network native asset. - `withdrawal.asset_is_known` (boolean): Whether the token is in the Fystack asset catalog. When `false`, `amount`, `value_usd`, and `asset_symbol` are empty, so deny unknown tokens explicitly if you need to. - `withdrawal.token_address` (string): Token contract address. Empty for native coins. - `withdrawal.destination_address` (string): Recipient address. - `withdrawal.network_code` (string · ref network): Internal network code. - `withdrawal.is_whitelisted` (boolean): Whether the destination address is in any workspace address book entry. - `withdrawal.address_book_id` (string · ref address_book): The address book entry that contains the destination address. One entry covers all of its addresses, so an entry works as a named list. - `withdrawal.address_age_sec` (number): Seconds since the destination address was added to its address book entry. Use this for cooling-off rules on newly added recipients. - `withdrawal.destination_is_internal` (boolean): Whether the destination is another wallet in the same workspace. - `withdrawal.destination_wallet_id` (string · ref wallet): The destination wallet when the destination is internal. ### Withdrawal velocity fields Available on `withdrawal.create` only. All values are in USD unless noted. - `wallet.outflow_1h / outflow_6h / outflow_24h` (number): Realized outflow for the source wallet over the last 1, 6, or 24 hours. - `wallet.withdrawal_count_1h` (integer): Withdrawal attempts from the source wallet in the last hour. - `wallet.pending_outflow` (number): Value of withdrawals from the source wallet that are not executed yet. - `wallet.projected_outflow_24h` (number): Realized 24-hour outflow, plus pending outflow, plus the withdrawal being checked, for the source wallet. - `wallet.asset_projected_outflow_24h` (number): The same projection for the source wallet in units of the withdrawn asset, for example at most 5 BTC per day. - `workspace.outflow_1h / outflow_6h / outflow_24h` (number): Realized outflow for the workspace over the last 1, 6, or 24 hours. - `workspace.withdrawal_count_1h` (integer): Workspace withdrawal attempts in the last hour. - `workspace.pending_outflow` (number): Value of workspace withdrawals that are not executed yet. - `workspace.projected_outflow_24h` (number): Workspace projection: realized 24-hour outflow, plus pending outflow, plus the withdrawal being checked. - `workspace.network_outflow_1h / network_outflow_6h / network_outflow_24h` (number): Realized workspace outflow on the current network over the last 1, 6, or 24 hours. - `workspace.network_withdrawal_count_1h` (integer): Workspace withdrawal attempts on the current network in the last hour. - `workspace.network_pending_outflow` (number): Pending workspace outflow on the current network. - `workspace.network_projected_outflow_24h` (number): Workspace projection on the current network. - `workspace.initiator_projected_outflow_24h` (number): Projection for withdrawals created by the same member or API key, across the workspace. Outflow and count fields are realized: they count a withdrawal only after it reaches an executed or successful terminal state. Pending outflow counts withdrawals that are waiting for approval, queued, or funding gas, whatever their age. > **Info.** When an active rule uses a pending or projected field, Fystack evaluates the > workspace's withdrawals one at a time, so two concurrent requests cannot both > pass a limit. A request that waits longer than 5 seconds for its turn is > rejected with HTTP `429` and can be retried. ### web3.contract_call fields - `resource.network_code` (string · ref network): Internal network code. - `resource.contract_address` (string): Lowercase contract address from the raw transaction `to` field. - `resource.method_selector` (string): Lowercase 4-byte selector, for example `0xa9059cbb`. - `resource.method_name` (string): Method name from request metadata when provided. - `resource.value_wei` (string): Native value sent with the call, represented as a string. - `resource.gas_limit` (integer): Gas limit from the raw transaction. - `resource.decoded_args` (map): Decoded arguments when Fystack recognizes the call. ERC-20 `transfer` exposes `to` and `value`. ### web3.sign_message fields - `resource.method` (string): Signing method. One of `personal_sign`, `eth_sign`, or `eth_signTypedData_v4`. - `resource.network_code` (string · ref network): Internal network code. - `resource.typed_data_primary_type` (string): EIP-712 primary type, for typed-data messages. - `resource.typed_data_domain_name` (string): EIP-712 domain name, for typed-data messages. - `resource.typed_data_verifying_contract` (string): EIP-712 verifying contract, for typed-data messages. - `resource.is_permit` (boolean): `true` for EIP-2612, DAI, and Permit2 messages, which let the spender move tokens later without another signature. - `resource.permit_spender` (string): The address the permit authorizes. - `resource.permit_tokens` (string\[]): Token contracts the permit covers. Use it with `in`, for example `"0x…" in resource.permit_tokens`. - `resource.permit_amount_is_unlimited` (boolean): `true` for max-value allowances (2159 or more, or a DAI permit with `allowed` set). - `resource.permit_deadline` (integer): Unix time the permit or its signature expires. `0` when the message has none. --- ## Approval lifecycle Create, update, and delete policy operations are approval-gated. The mutation creates a `policy` approval request and returns `status: "pending_approval"` with an `approval_request_id`. The policy change is applied only after the approval group reaches threshold. Rule create, update, and delete operations follow the same model when the parent policy has an approval group. The pending revision stores field-level diffs for audit and review. Updating wallet targeting through `/wallets` applies immediately after validation and cache invalidation. Use the policy update endpoint when you want wallet targeting changes reviewed as part of a larger policy revision. --- ## Authentication Policy API endpoints accept either a signed API-key request or a bearer access token. - `ACCESS-API-KEY` (header): API key UUID for HMAC-authenticated requests. - `ACCESS-TIMESTAMP` (header): UNIX timestamp in seconds used to build the HMAC signature. - `ACCESS-SIGN` (header): Base64-encoded HMAC signature. See [Authentication](https://docs.fystack.io/authentication.md) for the signing format. - `Authorization` (header): Bearer access token for user-session requests. > **Warning.** Policy approval decision endpoints remain user-session endpoints. Use bearer access tokens for approval request approve and reject actions. ## Response envelope Successful JSON responses use the standard Fystack envelope. - `success` (boolean): `true` when the request succeeds. - `message` (string): Response message, usually `success`. - `code` (integer): Numeric result code. Success responses use `0`. - `data` (object | array | null): Endpoint-specific response payload. Validation errors use the same envelope with `success: false` and an `errors` array. ## Data types ### Policy - `id` (string): Policy UUID. - `created_at` (string): ISO timestamp when the policy was created. - `updated_at` (string): ISO timestamp when the policy was last updated. - `name` (string): Policy name. 3 to 100 characters. - `description` (string): Optional description. Up to 500 characters. - `status` (string): One of `pending`, `rejected`, `active`, or `inactive`. - `version` (integer): Policy version number. - `workspace_id` (string): Workspace UUID. - `approval_group_id` (string | null): Approval group UUID assigned to the policy. - `wallet_ids` (string\[]): Target wallet UUIDs. Empty array means the policy targets all wallets in the workspace. - `rules` (PolicyRule\[]): Present on endpoints that preload rules. - `approval_group` (object): Present on endpoints that preload the approval group. - `pending_approval_request` (object): Present when there is a pending approval request for the policy. ### PolicyRule - `id` (string): Rule UUID. - `policy_id` (string): Parent policy UUID. - `trigger_action` (string): One of `withdrawal.create`, `web3.transfer`, `web3.contract_call`, or `web3.sign_message`. - `condition` (string): Boolean condition expression. 1 to 4000 characters. - `effect` (string): One of `ALLOW` or `DENY`. - `description` (string): Optional description. Up to 500 characters. - `is_enabled` (boolean): Whether the rule is enabled. Defaults to `true` on create when omitted. ### Mutation response Policy and rule mutation endpoints return a mutation payload in `data`. - `status` (string): One of `applied`, `pending_approval`, or `deleted`, depending on the operation. - `policy` (Policy): Returned by policy mutation endpoints when applicable. - `rule` (PolicyRule): Returned by rule mutation endpoints when applicable. - `approval_request_id` (string): Returned when the mutation is waiting for approval. ## Get the policy schema **Endpoint:** `GET /policy/schema` Returns fields a condition may reference, grouped by `trigger_action` and section. Use this endpoint for condition builders and client-side previews. Each field has a `name` and `type`. Optional keys: - `enum` (string\[]): The allowed values for the field. - `ref` (string): The entity the field refers to. Render a picker for it. See [Reference fields](#reference-fields). - `deprecated` (boolean): Still evaluated for existing rules. Hide it from new-rule builders. **Request** `GET /policy/schema` ```bash curl -X GET https://api.fystack.io/api/v1/policy/schema \ -H "ACCESS-API-KEY: your_api_key" \ -H "ACCESS-TIMESTAMP: 1667836889" \ -H "ACCESS-SIGN: YourBase64EncodedSignature==" ``` ```json { "success": true, "message": "success", "code": 0, "data": { "withdrawal.create": { "principal": [ { "name": "user_email", "type": "string", "deprecated": true }, { "name": "user_id", "type": "string", "ref": "workspace_user" }, { "name": "role", "type": "string", "enum": ["owner", "admin", "signer", "proposer", "viewer"] }, { "name": "initiator_type", "type": "string", "enum": ["user", "api_key"] }, { "name": "api_key_id", "type": "string", "ref": "api_key" } ], "withdrawal": [ { "name": "amount", "type": "number" }, { "name": "value_usd", "type": "number" }, { "name": "asset_symbol", "type": "string", "ref": "asset_symbol" }, { "name": "asset_is_native", "type": "boolean" }, { "name": "asset_is_known", "type": "boolean" }, { "name": "token_address", "type": "string" }, { "name": "destination_address", "type": "string" }, { "name": "network_code", "type": "string", "ref": "network" }, { "name": "is_whitelisted", "type": "boolean" }, { "name": "address_age_sec", "type": "number" }, { "name": "address_book_id", "type": "string", "ref": "address_book" }, { "name": "destination_is_internal", "type": "boolean" }, { "name": "destination_wallet_id", "type": "string", "ref": "wallet" } ], "wallet": [ { "name": "id", "type": "string", "ref": "wallet" }, { "name": "purpose", "type": "string", "enum": ["general", "custody", "payment", "user", "one_time_use", "gas_tank", "deployment"] }, { "name": "type", "type": "string", "enum": ["standard", "mpc"] }, { "name": "outflow_1h", "type": "number" }, { "name": "outflow_6h", "type": "number" }, { "name": "outflow_24h", "type": "number" }, { "name": "withdrawal_count_1h", "type": "integer" }, { "name": "pending_outflow", "type": "number" }, { "name": "projected_outflow_24h", "type": "number" }, { "name": "asset_projected_outflow_24h", "type": "number" } ], "workspace": [ { "name": "outflow_1h", "type": "number" }, { "name": "outflow_6h", "type": "number" }, { "name": "outflow_24h", "type": "number" }, { "name": "withdrawal_count_1h", "type": "integer" }, { "name": "network_outflow_1h", "type": "number" }, { "name": "network_outflow_6h", "type": "number" }, { "name": "network_outflow_24h", "type": "number" }, { "name": "network_withdrawal_count_1h", "type": "integer" }, { "name": "pending_outflow", "type": "number" }, { "name": "projected_outflow_24h", "type": "number" }, { "name": "network_pending_outflow", "type": "number" }, { "name": "network_projected_outflow_24h", "type": "number" }, { "name": "initiator_projected_outflow_24h", "type": "number" } ], "context": [ { "name": "hour", "type": "integer" }, { "name": "day_of_week", "type": "string" } ] }, "web3.transfer": { "principal": [ "... same as withdrawal.create ..." ], "withdrawal": [ "... same as withdrawal.create ..." ], "wallet": [ { "name": "id", "type": "string", "ref": "wallet" }, { "name": "purpose", "type": "string", "enum": ["general", "custody", "payment", "user", "one_time_use", "gas_tank", "deployment"] }, { "name": "type", "type": "string", "enum": ["standard", "mpc"] } ], "context": [ "... same as withdrawal.create ..." ] }, "web3.contract_call": { "principal": [ "... same as withdrawal.create ..." ], "resource": [ { "name": "network_code", "type": "string", "ref": "network" }, { "name": "contract_address", "type": "string" }, { "name": "method_selector", "type": "string" }, { "name": "method_name", "type": "string" }, { "name": "value_wei", "type": "string" }, { "name": "gas_limit", "type": "integer" }, { "name": "decoded_args", "type": "map" } ], "wallet": [ "... id, purpose, type ..." ], "context": [ "... same as withdrawal.create ..." ] }, "web3.sign_message": { "principal": [ "... same as withdrawal.create ..." ], "resource": [ { "name": "method", "type": "string", "enum": ["personal_sign", "eth_sign", "eth_signTypedData_v4"] }, { "name": "network_code", "type": "string", "ref": "network" }, { "name": "typed_data_primary_type", "type": "string" }, { "name": "typed_data_domain_name", "type": "string" }, { "name": "typed_data_verifying_contract", "type": "string" }, { "name": "is_permit", "type": "boolean" }, { "name": "permit_spender", "type": "string" }, { "name": "permit_tokens", "type": "string[]" }, { "name": "permit_amount_is_unlimited", "type": "boolean" }, { "name": "permit_deadline", "type": "integer" } ], "wallet": [ "... id, purpose, type ..." ], "context": [ "... same as withdrawal.create ..." ] } } } ``` ## Validate a rule **Endpoint:** `POST /workspaces/{workspace_id}/policies/rules/validate` Validates a rule body without creating or updating a rule. ### Path parameters - `workspace_id` (string): Workspace UUID. ### Body parameters - `trigger_action` (string): Required. One of `withdrawal.create`, `web3.transfer`, `web3.contract_call`, or `web3.sign_message`. - `condition` (string): Required. 1 to 4000 characters. Must compile to a boolean expression. - `effect` (string): Required. One of `ALLOW` or `DENY`. `web3.sign_message` rules are deny only. ### Response data - `valid` (boolean): `true` when the rule body validates. **Request** `POST /workspaces/{workspace_id}/policies/rules/validate` ```bash curl -X POST https://api.fystack.io/api/v1/workspaces/{workspace_id}/policies/rules/validate \ -H "ACCESS-API-KEY: your_api_key" \ -H "ACCESS-TIMESTAMP: 1667836889" \ -H "ACCESS-SIGN: YourBase64EncodedSignature==" \ -H "Content-Type: application/json" \ -d '{ "trigger_action": "withdrawal.create", "condition": "withdrawal.value_usd > 10000", "effect": "DENY" }' ``` ```json { "success": true, "message": "success", "code": 0, "data": { "valid": true } } ``` ## Create a policy **Endpoint:** `POST /workspaces/{workspace_id}/policies` Creates a policy mutation. ### Path parameters - `workspace_id` (string): Workspace UUID. ### Body parameters - `name` (string): Required. 3 to 100 characters. - `description` (string): Optional. Up to 500 characters. - `approval_group_id` (string): Required approval group UUID. - `wallet_ids` (string\[]): Optional. Up to 500 wallet UUIDs. - `rules` (PolicyRuleCreate\[]): Optional. Up to 50 rules. **Request** `POST /workspaces/{workspace_id}/policies` ```bash curl -X POST https://api.fystack.io/api/v1/workspaces/{workspace_id}/policies \ -H "ACCESS-API-KEY: your_api_key" \ -H "ACCESS-TIMESTAMP: 1667836889" \ -H "ACCESS-SIGN: YourBase64EncodedSignature==" \ -H "Content-Type: application/json" \ -d '{ "name": "Large withdrawal guard", "approval_group_id": "a1b2c3d4-e5f6-7890-1234-567890abcdef", "wallet_ids": [], "rules": [ { "trigger_action": "withdrawal.create", "condition": "withdrawal.value_usd > 10000", "effect": "DENY" } ] }' ``` ```json { "success": true, "message": "success", "code": 0, "data": { "status": "pending_approval", "approval_request_id": "b7e2a1c4-9d8f-4a3b-8c1d-2e3f4a5b6c7d", "policy": { "id": "9c8b7a6d-5e4f-3210-9a8b-7c6d5e4f3210", "name": "Large withdrawal guard", "status": "pending", "version": 1, "wallet_ids": [] } } } ``` ## List policies **Endpoint:** `GET /workspaces/{workspace_id}/policies` Returns policies in a workspace. ### Path parameters - `workspace_id` (string): Workspace UUID. ### Query parameters - `status` (string): Optional. One of `pending`, `rejected`, `active`, or `inactive`. - `limit` (integer): Optional. Maximum `1000`. - `offset` (integer): Optional. Maximum `100000`. ### Response data - `data` (Policy\[]): Array of policy objects. **Request** `GET /workspaces/{workspace_id}/policies` ```bash curl -X GET "https://api.fystack.io/api/v1/workspaces/{workspace_id}/policies?status=active&limit=20&offset=0" \ -H "ACCESS-API-KEY: your_api_key" \ -H "ACCESS-TIMESTAMP: 1667836889" \ -H "ACCESS-SIGN: YourBase64EncodedSignature==" ``` ## Get a policy **Endpoint:** `GET /workspaces/{workspace_id}/policies/{policy_id}` Returns a single policy. ### Path parameters - `workspace_id` (string): Workspace UUID. - `policy_id` (string): Policy UUID. ### Response data - `data` (Policy): Policy object, including rules, approval group, and pending approval request when present. **Request** `GET /workspaces/{workspace_id}/policies/{policy_id}` ```bash curl -X GET https://api.fystack.io/api/v1/workspaces/{workspace_id}/policies/{policy_id} \ -H "ACCESS-API-KEY: your_api_key" \ -H "ACCESS-TIMESTAMP: 1667836889" \ -H "ACCESS-SIGN: YourBase64EncodedSignature==" ``` ## Update a policy **Endpoint:** `PUT /workspaces/{workspace_id}/policies/{policy_id}` Updates policy fields. ### Path parameters - `workspace_id` (string): Workspace UUID. - `policy_id` (string): Policy UUID. ### Body parameters - `name` (string): Optional. 3 to 100 characters. - `description` (string): Optional. Up to 500 characters. - `approval_group_id` (string): Optional approval group UUID. - `wallet_ids` (string\[]): Optional. Up to 500 wallet UUIDs. Send an empty array to target all wallets. - `rules` (PolicyRuleCreate\[]): Optional. Replaces the complete rule set when present. Send an empty array to clear all rules. **Request** `PUT /workspaces/{workspace_id}/policies/{policy_id}` ```bash curl -X PUT https://api.fystack.io/api/v1/workspaces/{workspace_id}/policies/{policy_id} \ -H "ACCESS-API-KEY: your_api_key" \ -H "ACCESS-TIMESTAMP: 1667836889" \ -H "ACCESS-SIGN: YourBase64EncodedSignature==" \ -H "Content-Type: application/json" \ -d '{ "description": "Updated policy description", "wallet_ids": ["e0851607-417a-475a-b399-5f3aa262b81d"] }' ``` ## Delete a policy **Endpoint:** `DELETE /workspaces/{workspace_id}/policies/{policy_id}` Deletes a policy or creates a delete mutation that waits for approval. ### Path parameters - `workspace_id` (string): Workspace UUID. - `policy_id` (string): Policy UUID. ### Response data - `data` (Mutation response): Policy mutation response. **Request** `DELETE /workspaces/{workspace_id}/policies/{policy_id}` ```bash curl -X DELETE https://api.fystack.io/api/v1/workspaces/{workspace_id}/policies/{policy_id} \ -H "ACCESS-API-KEY: your_api_key" \ -H "ACCESS-TIMESTAMP: 1667836889" \ -H "ACCESS-SIGN: YourBase64EncodedSignature==" ``` ## Get policy history **Endpoint:** `GET /workspaces/{workspace_id}/policies/{policy_id}/history` Returns policy revisions for one policy. ### Path parameters - `workspace_id` (string): Workspace UUID. - `policy_id` (string): Policy UUID. ### Response data - `data` (PolicyRevision\[]): Array of policy revisions, including field-level `changes` when present. **Request** `GET /workspaces/{workspace_id}/policies/{policy_id}/history` ```bash curl -X GET https://api.fystack.io/api/v1/workspaces/{workspace_id}/policies/{policy_id}/history \ -H "ACCESS-API-KEY: your_api_key" \ -H "ACCESS-TIMESTAMP: 1667836889" \ -H "ACCESS-SIGN: YourBase64EncodedSignature==" ``` ## Get policy wallets **Endpoint:** `GET /workspaces/{workspace_id}/policies/{policy_id}/wallets` Returns the policy with its current `wallet_ids`. ### Path parameters - `workspace_id` (string): Workspace UUID. - `policy_id` (string): Policy UUID. ### Response data - `data` (Policy): Policy object. **Request** `GET /workspaces/{workspace_id}/policies/{policy_id}/wallets` ```bash curl -X GET https://api.fystack.io/api/v1/workspaces/{workspace_id}/policies/{policy_id}/wallets \ -H "ACCESS-API-KEY: your_api_key" \ -H "ACCESS-TIMESTAMP: 1667836889" \ -H "ACCESS-SIGN: YourBase64EncodedSignature==" ``` ## Update policy wallets **Endpoint:** `PUT /workspaces/{workspace_id}/policies/{policy_id}/wallets` Replaces a policy's wallet targeting list. ### Path parameters - `workspace_id` (string): Workspace UUID. - `policy_id` (string): Policy UUID. ### Body parameters - `wallet_ids` (string\[]): Optional. Up to 500 wallet UUIDs. Send an empty array to target all wallets. **Request** `PUT /workspaces/{workspace_id}/policies/{policy_id}/wallets` ```bash curl -X PUT https://api.fystack.io/api/v1/workspaces/{workspace_id}/policies/{policy_id}/wallets \ -H "ACCESS-API-KEY: your_api_key" \ -H "ACCESS-TIMESTAMP: 1667836889" \ -H "ACCESS-SIGN: YourBase64EncodedSignature==" \ -H "Content-Type: application/json" \ -d '{ "wallet_ids": ["e0851607-417a-475a-b399-5f3aa262b81d"] }' ``` ## Create a policy rule **Endpoint:** `POST /workspaces/{workspace_id}/policies/{policy_id}/rules` Creates one rule under an existing policy. ### Path parameters - `workspace_id` (string): Workspace UUID. - `policy_id` (string): Policy UUID. ### Body parameters - `id` (string): Optional rule UUID. - `trigger_action` (string): Required. One of `withdrawal.create`, `web3.transfer`, `web3.contract_call`, or `web3.sign_message`. - `condition` (string): Required. 1 to 4000 characters. - `effect` (string): Required. One of `ALLOW` or `DENY`. `web3.sign_message` rules are deny only. - `description` (string): Optional. Up to 500 characters. - `is_enabled` (boolean): Optional. Defaults to `true`. **Request** `POST /workspaces/{workspace_id}/policies/{policy_id}/rules` ```bash curl -X POST https://api.fystack.io/api/v1/workspaces/{workspace_id}/policies/{policy_id}/rules \ -H "ACCESS-API-KEY: your_api_key" \ -H "ACCESS-TIMESTAMP: 1667836889" \ -H "ACCESS-SIGN: YourBase64EncodedSignature==" \ -H "Content-Type: application/json" \ -d '{ "trigger_action": "web3.contract_call", "condition": "resource.gas_limit > 100000", "effect": "DENY", "is_enabled": true }' ``` ## Update a policy rule **Endpoint:** `PUT /workspaces/{workspace_id}/policies/rules/{rule_id}` Updates mutable rule fields. The trigger action is immutable; delete and recreate the rule to change it. ### Path parameters - `workspace_id` (string): Workspace UUID. - `rule_id` (string): Rule UUID. ### Body parameters - `condition` (string): Optional. 1 to 4000 characters. - `effect` (string): Optional. One of `ALLOW` or `DENY`. - `description` (string): Optional. Up to 500 characters. - `is_enabled` (boolean): Optional. **Request** `PUT /workspaces/{workspace_id}/policies/rules/{rule_id}` ```bash curl -X PUT https://api.fystack.io/api/v1/workspaces/{workspace_id}/policies/rules/{rule_id} \ -H "ACCESS-API-KEY: your_api_key" \ -H "ACCESS-TIMESTAMP: 1667836889" \ -H "ACCESS-SIGN: YourBase64EncodedSignature==" \ -H "Content-Type: application/json" \ -d '{ "condition": "resource.gas_limit > 150000", "is_enabled": true }' ``` ## Delete a policy rule **Endpoint:** `DELETE /workspaces/{workspace_id}/policies/rules/{rule_id}` Deletes one policy rule or creates a rule deletion mutation that waits for approval. ### Path parameters - `workspace_id` (string): Workspace UUID. - `rule_id` (string): Rule UUID. ### Response data - `data` (Mutation response): Rule mutation response. **Request** `DELETE /workspaces/{workspace_id}/policies/rules/{rule_id}` ```bash curl -X DELETE https://api.fystack.io/api/v1/workspaces/{workspace_id}/policies/rules/{rule_id} \ -H "ACCESS-API-KEY: your_api_key" \ -H "ACCESS-TIMESTAMP: 1667836889" \ -H "ACCESS-SIGN: YourBase64EncodedSignature==" ``` ## Design patterns Use `DENY` rules for hard controls: sanctions lists, unknown recipients, new address book entries, projected outflow limits, unknown tokens, unlimited permits, or high-risk contract methods. Use `ALLOW` rules narrowly. A matching `ALLOW` skips approval, so conditions should be precise: low USD value, trusted initiator, known wallet target, known network, and a known recipient with an address age check, or a known contract and method. Pair every `ALLOW` with a `DENY` ceiling, such as `wallet.projected_outflow_24h` or `workspace.initiator_projected_outflow_24h`. Because `DENY` always wins, the ceiling also caps the auto-approved path. Keep policy sets small and explicit. Broad workspace-wide policies are useful for baseline controls; wallet-scoped policies are better when a hot wallet, treasury wallet, or customer wallet has different risk thresholds. See [Policy Templates](https://docs.fystack.io/product/policies.md#policy-templates) for complete examples. --- # MPCium is a highly resilient, secure MPC engine Mpcium is a high-performance framework written in Go that implements secure distributed threshold signature schemes using Multi-Party Computation (MPC). Unlike traditional cryptographic systems that store private keys in a single location, Mpcium distributes private key fragments across multiple independent nodes, requiring cooperation from a threshold number of these nodes to perform cryptographic operations like transaction signing. This architecture provides: - **Enhanced Security:** No single point of compromise - **Distributed Trust:** No need to trust any single entity - **Fault Tolerance:** System remains operational even if some nodes fail - **Privacy Preservation:** No single node possesses the complete key At its cryptographic core, Mpcium integrates tss-lib, a production-grade threshold signature scheme library developed by Binance. It supports: - **ECDSA (secp256k1)**: Bitcoin, Ethereum, BNB, Polygon, and EVM-compatible L2 chains - **EdDSA (Ed25519)**: for Solana, Polkadot, Cardano, and other modern blockchains ## Archtecture documentation _Image: MPCIUM img_ 👉 [Detailed Architecture Document](https://deepwiki.com/fystack/mpcium) ## Threshold & Nodes Mpcium uses a **t-of-n threshold scheme** to securely generate and sign with private keys. - `n` = total number of MPC nodes (key shares) - `t` = minimum number of nodes required to sign Only `t` out of `n` nodes need to participate — the full private key is never reconstructed. To maintain security against compromised nodes, Mpcium enforces: ``` t ≥ ⌊n / 2⌋ + 1 ``` ### Example: 2-of-3 Threshold - ✅ `node0 + node1` → signs successfully - ✅ `node1 + node2` → signs successfully - ❌ `node0` alone → not enough shares This ensures: - No single point of compromise - Fault tolerance if some nodes go offline - Configurable security by adjusting `t` and `n` ## Architecture ### Overview Each Mpcium node: - Holds a **key share** in local AES-256 encrypted storage (via Badger KV) - Participates in **threshold signing** using `tss-lib` - Communicates over a **resilient messaging layer** using NATS - Registers itself with **Consul** for service discovery and health checks - Verifies incoming messages using **Ed25519-based mutual authentication** ### Message Flow & Signature Verification 1. A signing request is broadcast to the MPC cluster through **NATS** as an authenticated event. Each node **verifies the sender's Ed25519 signature** before processing the request. 2. NATS broadcasts the request to the MPC nodes. 3. Each participating node verifies: - The **signature** of the sender (Ed25519) - The **authenticity** of the message (non-replayable, unique session) 4. If the node is healthy and within the quorum (`t`), it: - Computes a partial signature using its share - Publishes the result back via NATS 5. Once `t` partial signatures are received, they are aggregated into a full signature. --- ### Properties - **No single point of compromise**: Keys are never fully assembled - **Byzantine-resilient**: Only `t` of `n` nodes are required to proceed - **Scalable and pluggable**: Easily expand the cluster or integrate additional tools - **Secure peer authentication**: All inter-node messages are signed and verified using Ed25519 ## Installation and Run [Full installation and run instructions](https://docs.fystack.io/mpcium/installation.md) ## Preview usage ### Start nodes ```shell $ mpcium start -n node0 $ mpcium start -n node1 $ mpcium start -n node2 ``` ### Client Implementations - **Go**: Available in the `pkg/client` directory. Check the `examples` folder for usage samples. - **TypeScript**: Available at [github.com/fystack/mpcium-client-ts](https://github.com/fystack/mpcium-client-ts) ### Client ```go import ( "github.com/fystack/mpcium/client" "github.com/nats-io/nats.go" ) func main () { natsConn, err := nats.Connect(natsURL) if err != nil { logger.Fatal("Failed to connect to NATS", err) } defer natsConn.Close() mpcClient := client.NewMPCClient(client.Options{ NatsConn: natsConn, KeyPath: "./event_initiator.key", }) err = mpcClient.OnWalletCreationResult(func(event mpc.KeygenSuccessEvent) { logger.Info("Received wallet creation result", "event", event) }) if err != nil { logger.Fatal("Failed to subscribe to wallet-creation results", err) } walletID := uuid.New().String() if err := mpcClient.CreateWallet(walletID); err != nil { logger.Fatal("CreateWallet failed", err) } logger.Info("CreateWallet sent, awaiting result...", "walletID", walletID) } ``` --- # Installing Mpcium This guide walks you through the process of installing and setting up MPCIUM nodes in your environment. ## Prerequisites Before starting, ensure you have: - **Go** 1.23+ installed: [Install Go here](https://go.dev/doc/install) - **NATS** server running - **Consul** server running --- ## Clone and Install Mpcium ### Clone the Repository ```bash git clone https://github.com/fystack/mpcium.git cd mpcium ``` ### Build the Project With Make: ```bash make ``` Or with Go: ```bash go install ./cmd/mpcium go install ./cmd/mpcium-cli ``` ### Available Commands - `mpcium`: Start an MPCium node - `mpcium-cli`: CLI utility for peer, identity, and initiator configuration --- ## Running NATS and Consul (Development Only) > ⚠️ This setup is insecure and should only be used for development. For production, use a secure cluster environment with TLS certificates. ### Docker Compose Configuration Create a `docker-compose.yaml` file: ```yaml version: "3" services: nats-server: image: nats:latest container_name: nats-server command: -js --http_port 8222 ports: - "4222:4222" - "8222:8222" - "6222:6222" tty: true restart: always consul: image: consul:1.15.4 container_name: consul ports: - "8500:8500" - "8601:8600/udp" command: "agent -server -ui -node=server-1 -bootstrap-expect=1 -client=0.0.0.0" restart: always ``` ### Start the Services ```bash docker compose up -d ``` --- ## Generate Peer Configuration ```bash mpcium-cli generate-peers -n 3 ``` Example output: ```json { "node0": "12345678-1234-1234-1234-123456789abc", "node1": "23456789-2345-2345-2345-23456789abcd", "node2": "34567890-3456-3456-3456-3456789abcde" } ``` --- ## Cluster Configuration ### 1. Create and Update `config.yaml` ```bash cp config.yaml.template config.yaml ``` Edit `config.yaml`: ```yaml nats: url: nats://127.0.0.1:4222 consul: address: localhost:8500 mpc_threshold: 2 environment: development badger_password: "your_badger_password" event_initiator_pubkey: "your_event_initiator_pubkey" ``` ### Generate a Strong Password (Recommended) ```bash < /dev/urandom tr -dc 'A-Za-z0-9!@#$^&*()-_=+[]{}|;:,.<>?/~' | head -c 16; echo ``` Example: ```yaml badger_password: "F))ysJp?E]ol&I;^" ``` ### 2. Register Peers to Consul ```bash mpcium-cli register-peers ``` --- ## Event Initiator Setup ### Generate the Initiator ```bash mpcium-cli generate-initiator ``` > 💡 Use `--encrypt` in production. ### Add Public Key to `config.yaml` From `event_initiator.identity.json`: ```json { "public_key": "09be5d070816aadaa1b6638cad33e819a8aed7101626f6bf1e0b427412c3408a" } ``` Update `config.yaml`: ```yaml event_initiator_pubkey: "09be5d070816aadaa1b6638cad33e819a8aed7101626f6bf1e0b427412c3408a" ``` --- ## Configure Node Identities ### 1. Create Node Folders ```bash mkdir node{0..2} for dir in node{0..2}; do cp config.yaml peers.json "$dir/"; done ``` ### 2. Generate Identity for Each Node Example for `node0`: ```bash cd node0 mpcium-cli generate-identity --node node0 ``` > 💡 For production, use encryption: > > ```bash > mpcium-cli generate-identity --node node0 --encrypt > ``` ### Generate Strong Password for Encryption ```bash < /dev/urandom tr -dc 'A-Za-z0-9!@#$^&*()-_=+[]{}|;:,.<>?/~' | head -c 16; echo ``` ### 3. Distribute Identity Files to All Nodes ```bash cp identity/node0_identity.json ../node1/identity/node0_identity.json cp identity/node0_identity.json ../node2/identity/node0_identity.json ``` Repeat this for `node1` and `node2`. ### Folder Structure Example ``` ├── node0 │   ├── config.yaml │   ├── identity │   │   ├── node0_identity.json │   │   ├── node0_private.key │   │   ├── node1_identity.json │   │   └── node2_identity.json │   └── peers.json ├── node1 │   ├── config.yaml │   ├── identity │   │   ├── node0_identity.json │   │   ├── node1_identity.json │   │   ├── node1_private.key │   │   └── node2_identity.json │   └── peers.json ├── node2 │   ├── config.yaml │   ├── identity │   │   ├── node0_identity.json │   │   ├── node1_identity.json │   │   ├── node2_identity.json │   │   └── node2_private.key │   └── peers.json ``` --- ## Start Mpcium Nodes Start each node: ```bash cd node0 mpcium start -n node0 ``` ```bash cd node1 mpcium start -n node1 ``` ```bash cd node2 mpcium start -n node2 ``` > 💡 In production, avoid hardcoded passwords: > > ```bash > mpcium start -n node0 --prompt-credentials > ``` --- _Image: All ndoe ready_ --- ## Production Deployment (High Security) 1. Use production-grade **NATS** and **Consul** clusters. 2. Enable **TLS certificates** on all endpoints. 3. Encrypt all keys: ```bash mpcium-cli generate-initiator --encrypt mpcium-cli generate-identity --node node0 --encrypt ``` 4. Use `--prompt-credentials` to securely input Badger passwords (avoid hardcoding in `config.yaml`). --- ## Apendix ### Decrypt initiator private key with age ``` age --decrypt -o event_initiator.key event_initiator.key.age ``` --- # Mpcium TypeScript Client A TypeScript client for interacting with Mpcium Multi-Party Computation (MPC) service to generate and manage wallets and sign transactions across multiple blockchains. ### Client sign _Image: Sign solana img_ ### MPCIUM nodes coordinate to sign transaction _Image: MPCIUM nodes_ ## Prerequisites Important: Before using this TypeScript client, you need to set up the Mpcium MPC nodes. The nodes provide the underlying MPC infrastructure that this client connects to. Please follow the installation and setup instructions to deploy the required MPC nodes. Typically, you'll need to run multiple nodes (e.g., 3 nodes in a 2-of-3 threshold setup) before using this client. [MPC nodes full installation and run instructions](https://docs.fystack.io/mpcium/installation.md) ## Overview Mpcium is a service that provides secure key management and transaction signing using Multi-Party Computation. This client library allows you to: - Generate cryptographic keys without exposing private key material - Sign transactions for multiple blockchain networks - Receive cryptographic operation results through event subscriptions ## Supported Key Types and Blockchains - **ECDSA**: Bitcoin, Ethereum, and other EVM-compatible chains - **EdDSA**: Solana, Polkadot, Cardano, and other EdDSA-based chains ## Installation ```bash npm install @fystack/mpcium-ts ``` ## Code example available on Github [MPC Client Typescript](https://github.com/fystack/mpcium-client-ts) ## Creating a client - User need to generate `event_initiator.key` through [mpcium-cli](https://github.com/fystack/mpcium/blob/master/INSTALLATION.md) before using this client ```ts import { connect } from "nats"; import { MpciumClient } from "@fystack/mpcium-ts"; async function setupClient() { // First, establish NATS connection const nc = await connect({ servers: "nats://localhost:4222" }); // Create client with key path const mpcClient = await MpciumClient.create({ nc: nc, keyPath: "./event_initiator.key", }); return mpcClient; } ``` ### Encrypted client (Recommended for production usage) ```ts const mpcClient = await MpciumClient.create({ nc: nc, keyPath: "./event_initiator.key.age", password: "your-secure-password", // Stored and load secure vault like KMS, Hashicorp vault }); ``` ### Generating a Wallet ```ts import { connect } from "nats"; import { MpciumClient } from "@fystack/mpcium-ts"; import fs from "fs"; async function generateWallet() { const nc = await connect({ servers: "nats://localhost:4222" }); const mpcClient = await MpciumClient.create({ nc: nc, keyPath: "./event_initiator.key", }); // Store wallets in a file (for testing only) const walletsFile = "./examples/wallets.json"; const wallets = fs.existsSync(walletsFile) ? JSON.parse(fs.readFileSync(walletsFile, "utf8")) : {}; // Subscribe to wallet creation results mpcClient.onWalletCreationResult((event) => { console.log("Received wallet creation result:", event); // Store wallet info with wallet_id as key wallets[event.wallet_id] = { wallet_id: event.wallet_id, ecdsa_pub_key: event.ecdsa_pub_key, eddsa_pub_key: event.eddsa_pub_key, }; // Save to file (in production, use a database) fs.writeFileSync(walletsFile, JSON.stringify(wallets, null, 2)); console.log(`Wallet ${event.wallet_id} saved to wallets.json`); console.log( "NOTE: File storage is for testing only. Use a database in production." ); }); // Create the wallet const walletID = await mpcClient.createWallet(); console.log(`CreateWallet request sent, wallet ID: ${walletID}`); // Wait for result and handle cleanup process.on("SIGINT", async () => { await mpcClient.cleanup(); await nc.drain(); process.exit(0); }); } ``` ### Signing a Solana Transaction ```ts import { connect } from "nats"; import { MpciumClient, KeyType } from "@fystack/mpcium-ts"; import { Connection, Transaction, SystemProgram, PublicKey, LAMPORTS_PER_SOL, } from "@solana/web3.js"; import fs from "fs"; async function signSolanaTransaction(walletId) { const nc = await connect({ servers: "nats://localhost:4222" }); const mpcClient = await MpciumClient.create({ nc: nc, keyPath: "./event_initiator.key", }); // Get wallet information from wallets.json const walletsFile = "./examples/wallets.json"; const wallets = JSON.parse(fs.readFileSync(walletsFile, "utf8")); const wallet = wallets[walletId]; if (!wallet) { throw new Error(`Wallet ${walletId} not found`); } // Connect to Solana devnet const connection = new Connection( "https://api.devnet.solana.com", "confirmed" ); // Create public key from wallet data const publicKeyBuffer = Buffer.from(wallet.eddsa_pub_key, "base64"); const fromPublicKey = new PublicKey(publicKeyBuffer); // Create transaction const transaction = new Transaction().add( SystemProgram.transfer({ fromPubkey: fromPublicKey, toPubkey: new PublicKey("4LKprD1XvTuBupHqWXoS42XsEBHp7qALo3giDBRCNhAV"), lamports: LAMPORTS_PER_SOL * 0.01, }) ); // Get recent blockhash const { blockhash } = await connection.getLatestBlockhash(); transaction.recentBlockhash = blockhash; transaction.feePayer = fromPublicKey; // Serialize transaction message const serializedTx = transaction.serializeMessage(); // Listen for signing results mpcClient.onSignResult((event) => { if (event.result_type === 1) { // Success const signature = Buffer.from(event.signature, "base64"); transaction.addSignature(fromPublicKey, signature); if (transaction.verifySignatures()) { connection .sendRawTransaction(transaction.serialize()) .then((txId) => console.log(`Transaction sent: ${txId}`)); } } }); // Send signing request await mpcClient.signTransaction({ walletId: walletId, keyType: KeyType.Ed25519, networkInternalCode: "solana:devnet", tx: Buffer.from(serializedTx).toString("base64"), }); } ``` [Full example: sign-solana.ts](https://github.com/fystack/mpcium-client-ts/blob/master/examples/sign-solana.ts) ## Tests ### 1. Generate an MPC wallet ``` npx ts-node ./examples/generate-wallet.ts ``` ``` Connected to NATS at localhost:4222 Subscribed to wallet creation results CreateWallet request sent for wallet: a99900b2-0ef8-4d7e-8c3f-2ef85abbae4c CreateWallet sent, awaiting result... walletID: a99900b2-0ef8-4d7e-8c3f-2ef85abbae4c Received wallet creation result: { wallet_id: 'a99900b2-0ef8-4d7e-8c3f-2ef85abbae4c', ecdsa_pub_key: '1ftOqTd9z540F7J6bSLJJ8gqn85HlyQKetWB4mACDFBhaodgiNr9ILL5wZ95yWpaqQc77f02rQklUeDSZhLVVA==', eddsa_pub_key: 'm0qUKZlxmzgYA9sRh1Q7cQJHT8jEdUQjbic8hQNHG2o=' } Solana wallet address: BTC9kvDchPvu84iMzLbhYBg5bWma4QHDAFumpF41ErjT Ethereum wallet address: 0x309bdE4d218e44E4a391f4c43Bf6226156D3255b Wallet saved to wallets.json with ID: a99900b2-0ef8-4d7e-8c3f-2ef85abbae4c ``` ### 2. Transfer Solana to the wallet - Use Phantom to transfer SOL from devnet to the wallet ### ### 3. Sign a Solana transaction ``` npx ts-node ./examples/sign-solana.ts a99900b2-0ef8-4d7e-8c3f-2ef85abbae4c ``` ### 4. Sign an Ethereum transaction ``` npx ts-node ./examples/sign-eth.ts a99900b2-0ef8-4d7e-8c3f-2ef85abbae4c ``` --- ## Mpcium Core Security Principles Mpcium implements a threshold signature scheme with industry-standard security practices to protect cryptographic operations: 1. **Distributed Trust**: No single entity possesses complete private keys 2. **Threshold Cryptography**: Requires t+1 nodes to participate in signing operations 3. **End-to-End Verification**: All communications are signed and verified 4. **Defense in Depth**: Multiple layers of encryption and verification ## Identity and Authentication ### Node Identity Management - **Ed25519 Keypairs**: Each node possesses a unique Ed25519 keypair for identity - **Identity Generation**: Secure identity creation with the `generate-identity` command: ``` go run cmd/mpcium-cli/main.go generate-identity --node=node0 --peers=peers.json --encrypt ``` - **Metadata Tracking**: Each identity includes creation information, machine details, and timestamps - **Identity Verification**: All operations require cryptographic proof of identity ### Message Authentication Every message in the Mpcium network undergoes rigorous verification: ```go // Messages are signed before transmission signature, err := s.identityStore.SignMessage(&tssMsg) if err != nil { s.ErrCh <- fmt.Errorf("failed to sign message: %w", err) return } tssMsg.Signature = signature // Messages are verified upon receipt if !s.verifyMessageSignature(message) { return fmt.Errorf("invalid message signature") } ``` - **Deterministic Signatures**: Messages are canonicalized before signing to ensure consistency - **Party ID Verification**: TSS protocol ensures all participating nodes have valid identities - **Signature Verification**: All inbound messages undergo signature verification before processing ## Encryption Technologies ### Private Key Encryption Private keys are protected using multiple mechanisms: 1. **Age Encryption**: - State-of-the-art file encryption tool based on X25519 - Scrypt key derivation for password-based encryption - Authenticated encryption ensuring tamper resistance ``` # Decrypt private key for use age --decrypt -o event_initiator.key event_initiator.key.age ``` 2. **Password Requirements**: - Generated with high entropy: ``` < /dev/urandom tr -dc 'A-Za-z0-9!@#$^&\*()-\_=+[]{}|;:,.<>?/~' | head -c 16; echo ``` - Users are prompted for passwords via secure input methods that don't echo to screen ### Local Storage Encryption All node-local sensitive data is encrypted: ```go // BadgerDB with mandatory encryption badgerKv, err := kvstore.NewBadgerKVStore( dbPath, []byte(viper.GetString("badger_password")), ) ``` - **Error on Missing Key**: The system refuses to start if encryption keys aren't provided - **Encrypted Index**: Both data and metadata are encrypted - **ZSTD Compression**: Data is compressed before encryption for efficiency ## Network Security ### TLS Implementation All production communications use TLS: ```yaml nats: url: tls://127.0.0.1:4222 # Required TLS for production username: "" password: "" consul: address: https://consul.example.com # Required HTTPS for production ``` - **Certificate Verification**: Consul and NATS connections verify server certificates - **Mutual TLS**: Both client and server authenticate each other - **Strong Cipher Suites**: Only modern, secure cipher suites are used ### Secure Messaging Architecture The NATS messaging infrastructure provides: - **Topic Isolation**: Each session uses unique topics - **Direct Messaging**: Point-to-point secure communications - **JetStream Durability**: Critical messages persist with encryption - **Queue Management**: Distributed queue processing with authentication ## Threshold Signature Protocol Security ### Multi-Party Computation The threshold signature implementation includes: - **Key Resharing**: Support for key rotation while maintaining the same public key - **Threshold Flexibility**: Configurable threshold based on security requirements - **Protocol Verification**: Messages include round and protocol verification ### Session Security Each signing or key generation session implements: - **Session Isolation**: Unique identifiers prevent cross-session interference - **Timeout Handling**: Sessions expire if not completed within a timeframe - **Error Propagation**: Secure error handling to prevent information leakage - **Round Verification**: Each protocol round validates expected inputs ## Operational Security ### Node Distribution - Deploy nodes across different: - Cloud providers - Geographic regions - Network segments - Administrative domains ### Key Management - **Separate Key Material**: Store encrypted keys separately from passwords - **Hardware Security**: Consider HSM integration for production deployments - **Key Rotation**: Establish regular key rotation procedures - **Backup Security**: Maintain secure, encrypted backups of all key material ### Network Hardening - **Firewall Rules**: Restrict communication to only necessary ports - **IP Whitelisting**: Limit connections to known IP addresses - **DDoS Protection**: Implement DDoS mitigation for public-facing components - **Network Monitoring**: Monitor for unusual traffic patterns ### Continuous Security - **Dependency Scanning**: Regularly scan and update dependencies - **Penetration Testing**: Conduct regular security assessments - **Audit Logging**: Maintain comprehensive audit logs of all operations - **Incident Response**: Develop and test security incident response procedures --- # Backup and Recovery MPCIUM provides robust backup and recovery capabilities to ensure your distributed MPC cluster data is protected against loss. The backup system uses an incremental backup strategy with encryption to efficiently store and secure your database state while minimizing resource usage. _Image: Incremental backup_ > **Warning.** **Important**: Always test your backup and recovery procedures in a non-production environment before implementing them in production. ## Backup Configuration MPCIUM supports configurable backup settings that can be adjusted in your `config.yaml` file: **Backup Configuration** ```yaml # Backup Configuration backup_enabled: true # Enable/disable automatic backups (default: true) backup_period_seconds: 300 # How often to perform backups in seconds (default: 300) backup_dir: "./backups" # Directory where encrypted backups are stored ``` ### Configuration Parameters **backup_enabled** (boolean) Controls whether automatic backups are performed. Set to `false` to disable backups entirely. **backup_period_seconds** (integer) Defines the interval between automatic backups in seconds. The default of 300 seconds (5 minutes) provides a good balance between data protection and system performance. **backup_dir** (string) Specifies the directory where encrypted backup files are stored. Ensure this directory has appropriate permissions and sufficient disk space. ## Backup Strategy MPCIUM implements an **incremental backup strategy** that efficiently stores only the changes since the last backup, reducing storage requirements and backup time. This approach ensures that each backup is not a duplicate of previous backups, saving significant storage space and network bandwidth while maintaining complete data protection. ### Backup File Structure Backup files are stored in the configured backup directory with the following naming convention: ``` node0/ └── backups/ ├── backup-node0-2025-07-28_20-46-39-1.enc ├── backup-node0-2025-07-28_20-48-39-2.enc ├── backup-node0-2025-07-28_20-49-39-3.enc └── latest.version ``` ### Backup File Format Each backup file contains: - **Incremental database changes** since the last backup (not full duplicates) - **Encrypted database state** using AES-256-GCM encryption for security during transit and storage - **Metadata** including creation timestamp, encryption parameters, and version information - **Chain of incremental updates** that can be reconstructed to restore the complete database state ### Backup Metadata Example ```json { "algo": "AES-256-GCM", "nonce_b64": "MgDMCSqKNujD+9na", "created_at": "2025-07-28T20:46:39+07:00", "since": 0, "next_since": 6, "encryption_key_id": "bd1937594bea2580" } ``` > **Info.** **Security**: Backup files are encrypted using the same `badger_password` used for database encryption, ensuring your backup data remains secure during transit and storage. The incremental approach combined with encryption provides both resource efficiency and data protection. ## Recovery Procedures ### Using mpcium-cli recover To recover your MPCIUM database from encrypted backup files, use the `mpcium-cli recover` command: **Recovery Command** ```bash mpcium-cli recover --help NAME: mpcium recover - Recover database from encrypted backup files USAGE: mpcium recover [command [command options]] OPTIONS: --backup-dir string, -b string Directory containing encrypted backup files --recovery-path string, -r string Target path for database recovery --backup-encryption-key string, -k string Encryption key for backup files (will prompt if not provided) --force, -f Force overwrite if recovery path already exists (default: false) --help, -h show help ``` ### Recovery Process 1. **Stop the MPCIUM node** to ensure no active database operations 2. **Run the recovery command** with appropriate parameters 3. **Update the configuration** to point to the recovered database 4. **Restart the node** with the recovered data _Image: Backup recovery_ **Recovery Example** ```bash # Step 1: Stop the MPCIUM node # Step 2: Recover the database mpcium-cli recover \ --backup-dir backups/ \ --recovery-path restore_db/node0 # Step 3: Update config.yaml # Change db_path to: db_path: "restore_db" # Step 4: Restart the node mpcium start -n node0 ``` ## AWS S3 Integration MPCIUM supports AWS S3 for cloud-based backup storage, providing additional redundancy and disaster recovery capabilities. ### Setting Up S3 Backup #### 1. Create S3 Bucket **Create S3 Bucket** ```bash aws s3 mb s3://node0-backup --region ap-southeast-1 ``` > **Info.** **Note**: Replace `ap-southeast-1` with your desired region. Bucket names must be globally unique. #### 2. Configure S3 Permissions Ensure your AWS credentials have appropriate permissions for S3 operations: **S3 Policy Example** ```json { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "s3:GetObject", "s3:PutObject", "s3:ListBucket", "s3:DeleteObject" ], "Resource": [ "arn:aws:s3:::node0-backup", "arn:aws:s3:::node0-backup/*" ] } ] } ``` ### S3 Backup Operations #### Upload Local Backups to S3 **Upload to S3** ```bash # Sync local backup folder to S3 aws s3 sync ./backups s3://node0-backup ``` #### Download Backups from S3 **Download from S3** ```bash # Sync from S3 to local folder aws s3 sync s3://node0-backup ./backups ``` #### Verify S3 Backup **Verify S3 Contents** ```bash # List S3 backup contents aws s3 ls s3://node0-backup --recursive # Check backup file details aws s3api head-object --bucket node0-backup --key backup-node0-2025-07-28_20-46-39-1.enc ``` Former: Product Manager at TomoChain labs ## Automated Backups ### Cron Job Setup Set up automated S3 backups using cron jobs to ensure regular cloud storage synchronization: **Cron Job Configuration** ```bash # Edit crontab crontab -e # Add backup job (runs every hour) 0 * * * * /usr/local/bin/backup-mpcium.sh # Add daily backup verification 0 2 * * * /usr/local/bin/verify-backup.sh ``` ### Backup Script Example Create a backup script to automate the S3 sync process: **Backup Script** ```bash #!/bin/bash # /usr/local/bin/backup-mpcium.sh # Configuration NODE_NAME="node0" S3_BUCKET="node0-backup" BACKUP_DIR="/opt/mpcium/node0/backups" LOG_FILE="/var/log/mpcium-backup.log" # Log function log() { echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" >> "$LOG_FILE" } # Start backup log "Starting MPCIUM backup to S3" # Sync local backups to S3 if aws s3 sync "$BACKUP_DIR" "s3://$S3_BUCKET" --delete; then log "Backup completed successfully" else log "Backup failed" exit 1 fi # Verify backup if aws s3 ls "s3://$S3_BUCKET" | grep -q "backup-$NODE_NAME"; then log "Backup verification successful" else log "Backup verification failed" exit 1 fi ``` ### Verification Script Create a script to verify backup integrity: **Verification Script** ```bash #!/bin/bash # /usr/local/bin/verify-backup.sh # Configuration S3_BUCKET="node0-backup" LOG_FILE="/var/log/mpcium-backup-verify.log" # Log function log() { echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" >> "$LOG_FILE" } # Check S3 bucket accessibility if aws s3 ls "s3://$S3_BUCKET" > /dev/null 2>&1; then log "S3 bucket accessible" else log "S3 bucket not accessible" exit 1 fi # Count backup files BACKUP_COUNT=$(aws s3 ls "s3://$S3_BUCKET" --recursive | grep -c "\.enc$") log "Found $BACKUP_COUNT backup files in S3" # Check for recent backups (within last 24 hours) RECENT_BACKUPS=$(aws s3 ls "s3://$S3_BUCKET" --recursive | grep "$(date '+%Y-%m-%d')" | wc -l) log "Found $RECENT_BACKUPS backups from today" if [ "$RECENT_BACKUPS" -eq 0 ]; then log "WARNING: No recent backups found" exit 1 fi ``` ### Monitoring and Alerts Set up monitoring for your backup processes: **Monitoring Setup** ```bash # Check backup script exit status if [ $? -ne 0 ]; then # Send alert (example with curl to webhook) curl -X POST -H "Content-Type: application/json" \ -d '{"text":"MPCIUM backup failed"}' \ https://your-webhook-url.com/alert fi ``` > **Success.** **Best Practice**: Test your backup and recovery procedures regularly in a testing environment to ensure they work correctly when needed. --- # Fystack Cosigner The cosigner is an MPC participant you run on your own infrastructure. It holds one share of every wallet key and joins keygen and signing with Fystack's [MPCium](https://docs.fystack.io/mpcium.md) nodes. Before each signature it asks your approval server. [Install the cosigner](https://docs.fystack.io/cosigner/installation.md) [Architecture](https://docs.fystack.io/cosigner/architecture.md) ## Why run a cosigner - **Shared custody**: Fystack cannot reach a signing quorum without your share. - **Your policy, enforced locally**: Your callback server sees the raw bytes to be signed and can reject anything. - **No inbound ports**: The cosigner dials out to Fystack over gRPC with TLS. - **Signed releases**: Every image and binary is Sigstore-signed. `cosigner-cli` verifies and pins the digest before running. - **Enclave isolation (coming soon)**: AWS Nitro Enclave support so host admins cannot read key shares. ## Overview _Diagram: Cosigner in your infrastructure connects outbound to the Fystack Gateway, asks your callback for approval and unwraps its storage key with AWS KMS_ - **Your side**: the cosigner container, your approval callback, and a storage key source (password or AWS KMS). - **Fystack side**: Apex (pairing, requests), Gateway (cosigner connections), Orchestrator (sessions) and MPCium nodes. MPC messages are end-to-end encrypted between participants. The Gateway and Orchestrator relay them but cannot read them. ## Operations | Operation | Cosigner role | | --- | --- | | Keygen | Generates its share of ECDSA (secp256k1) and EdDSA (Ed25519) keys. No one ever holds the full key. | | Sign | Verifies the request, asks your callback, then joins threshold signing. | | Reshare | Rotates shares to a new committee. Public key and addresses stay the same. | | Export | Writes encrypted `.mshare` files for offline recovery. | ## Deployment modes | Mode | Runs on | Who can read key shares | | --- | --- | --- | | Regular | Linux + Docker, managed by `cosigner-cli` | Anyone with root or Docker access on the host | | Nitro Enclave (coming soon) | AWS EC2 with Nitro Enclaves | Only the attested enclave | > **Note:** Regular mode is the supported deployment today. [Nitro Enclave](https://docs.fystack.io/cosigner/nitro-enclave.md) support is coming soon. ## Docs - [Architecture](https://docs.fystack.io/cosigner/architecture.md): Components, channels and session lifecycle. - [Key Lifecycle & Signing](https://docs.fystack.io/cosigner/signing-flow.md): Keygen, signing and resharing, step by step. - [Approval Callback](https://docs.fystack.io/cosigner/approval-callback.md): Approve or reject every operation from your own server. - [Installation](https://docs.fystack.io/cosigner/installation.md): Install, pair and start a cosigner. - [Operations](https://docs.fystack.io/cosigner/operations.md): CLI reference, backups, updates and monitoring. - [Nitro Enclave Deployment](https://docs.fystack.io/cosigner/nitro-enclave.md): Coming soon: run the cosigner in an AWS Nitro Enclave. - [Security Model](https://docs.fystack.io/cosigner/security.md): Trust boundaries, encryption and verification. --- # Cosigner Architecture What runs where, how the parts talk, and how a session is coordinated. _Diagram: Cosigner architecture overview_ ## Components ### Cosigner runtime One Go binary, shipped as `ghcr.io/fystack/cosigner` for `linux/amd64` and `linux/arm64`. It joins sessions; it does not coordinate them. - **MPC engine**: Fystack's fork of [tss-lib](https://github.com/bnb-chain/tss-lib), same as MPCium. ECDSA uses GG18 on secp256k1. EdDSA uses tss-lib threshold Ed25519. - **Identity key**: Ed25519, created at pairing. Authenticates the cosigner and signs its messages. - **Key store**: AES-256-GCM encrypted rows in SQLite (default), Badger or DynamoDB. - **Gateway relay**: One outbound gRPC stream. Sends presence every 5 seconds. - **Approval client**: Calls your [approval callback](https://docs.fystack.io/cosigner/approval-callback.md) before every session. - **Audit log**: Signed, hash-chained JSONL. ### Your services - **cosigner-cli**: Installs, verifies, pairs, starts and backs up the runtime. - **Approval callback**: Your HTTPS server. Returns `approve` or `reject`. - **Storage key source**: A password (`manual` profile) or AWS KMS (`unattended` profile). ### Fystack services - **Apex**: Issues pairing tokens, registers cosigner keys, handles revocation, submits MPC requests. - **Gateway**: Accepts cosigner streams, checks identity and revocation, bridges traffic to NATS. One live stream per cosigner across all replicas. - **Orchestrator**: Runs the session state machine and signs every control message. Never touches key shares. - **MPCium**: Fystack's MPC nodes. See [MPCium](https://docs.fystack.io/mpcium.md). ## Channels | From → to | Transport | Auth | Carries | | --- | --- | --- | --- | | Cosigner → Apex | HTTPS | One-time pairing token | Identity registration | | Cosigner ↔ Gateway | gRPC stream, TLS | EdDSA JWT, 60s lifetime | All session traffic | | Gateway ↔ Orchestrator ↔ MPCium | NATS | Ed25519 per message | Coordination inside Fystack | | Participant ↔ participant | Relayed | X25519 + ChaCha20-Poly1305 | MPC rounds, end-to-end encrypted | | Cosigner → Callback | HTTPS | RS256 JWT both ways | Approve or reject | ### Stream envelopes Every frame carries the authenticated `workspace_id`, `cosigner_id` and `participant_id`. The Gateway drops frames that don't match the stream identity. | Kind | Direction | Subject | | --- | --- | --- | | `CONTROL` | Orchestrator → cosigner | `mpc.v1.peer..control` | | `SESSION_EVENT` | Cosigner → orchestrator | `mpc.v1.session..event` | | `P2P` | Participant ↔ participant | `mpc.v1.peer..session..p2p` | | `PRESENCE` | Cosigner → orchestrator | `mpc.v1.peer..presence` | ## Trust boundaries - **Gateway**: Relays ciphertext. Can drop or delay traffic, cannot read or forge MPC messages. - **Orchestrator**: Signs every control message with a key the cosigner pinned at pairing. Unsigned or mis-signed messages are dropped. - **Apex**: Cannot skip your callback. Every session goes to your callback first. - **Your cosigner**: Cannot sign alone. The committee is fixed at keygen. Details in [Security Model](https://docs.fystack.io/cosigner/security.md). ## Sessions Each keygen, sign or reshare is one orchestrator session with a 120 second TTL. _Diagram: Session states: created, waiting, key_exchange, active_mpc, completed, with failed and expired_ 1. **Admission**: Orchestrator verifies the Apex signature, checks the committee and confirms every participant is online. One session per wallet and protocol at a time. 2. **waiting**: Participants receive a signed `session_start`, run local checks (the cosigner calls your callback) and reply `peer_joined`, `peer_ready`. 3. **key_exchange**: Participants exchange signed X25519 keys. Wallets with an approver also need its signed approval here. 4. **active_mpc**: Orchestrator sends `mpc_begin`. Participants run tss-lib rounds directly. 5. **completed**: All participants report the same result hash. Orchestrator signs and publishes the result. A session **fails** on a participant error, a disconnect or mismatched results, and **expires** after 120 seconds. Either way the orchestrator broadcasts a signed `session_abort`. ### Committees Threshold `t` means `t + 1` signers (tss-lib semantics). The committee of MPCium nodes and cosigners is fixed at keygen. Every later session must use the same party keys, identity keys and approver roles. ### Connections - One live stream per cosigner identity. A second instance with the same identity is rejected. - Each session is bound to the stream that was live when it started. If that stream drops, the session fails with `PARTICIPANT_DISCONNECTED`. - A revoked cosigner gets `PermissionDenied` and stops. --- # Key Lifecycle & Signing Flow Keygen, signing and resharing from the cosigner's side. ## Keygen Creating a wallet in a workspace with a cosigner runs distributed key generation across the MPCium nodes and your cosigner. 1. Orchestrator sends a signed `session_start` with `KEYGEN`, the `key_id`, protocol and threshold. 2. Cosigner verifies the signature and asks your callback. 3. Participants run tss-lib keygen. Each ends with its own share and the shared public key. 4. Cosigner encrypts its share, stores it as `shares::` and records the committee. 5. Only the public key and a result hash leave the cosigner. 6. Orchestrator checks all public keys match and returns them to Apex. Wallets needing both ECDSA and EdDSA run two linked sessions. If one fails, both abort. > **Note:** ECDSA needs Paillier pre-parameters. The cosigner generates them on first boot (up to 5 minutes) and reports not ready until done. ## Signing _Diagram: Signing sequence: Apex requests, orchestrator starts the session, cosigner asks the callback, participants sign, orchestrator returns the signature_ ### 1. Admission A request that passes your workspace [policies](https://docs.fystack.io/product/policies.md) goes from Apex to the orchestrator, signed with the Apex initiator key. The orchestrator checks the committee against the keygen record, confirms everyone is online, and sends a signed `session_start`. `session_start` carries `key_id`, the raw `signing_input`, optional `signing_context` (transaction details, max 64 KiB), HD `derivation`, threshold and participants. ### 2. Local checks The cosigner rejects the session unless all pass, in order: 1. Message under 128 KiB. 2. Valid orchestrator signature (key pinned at pairing). 3. Under 20 starts and 10 aborts per minute. 4. Under 5 concurrent sessions. 5. `session_start` written to the audit log. 6. Your callback returns `approve`. Otherwise the session fails with `CALLBACK_REJECTED`. If the orchestrator aborts while the callback is pending, the cosigner stops waiting. ### 3. Key exchange - Session must match the pinned committee: threshold, party keys, identity keys, approver flags. - Cosigner sends `peer_joined`, `peer_ready` and a signed X25519 key to each peer. - Each peer pair gets a per-direction ChaCha20-Poly1305 key (HKDF over X25519, bound to the session). ### 4. Threshold signing The orchestrator sends `mpc_begin`. If the wallet has an approver (for example a Fystack mobile approver), `mpc_begin` must include its signed approval of this exact signing input. Without it the cosigner fails the session with `APPROVAL_REQUIRED`. Participants then run tss-lib rounds. Every message is signed and sequence-numbered, so replays are dropped. ### 5. Result Each participant reports a hash of its signature. The orchestrator requires all hashes to match, then returns the signed result (`signature`, `r`, `s`, recovery id) to Apex. The cosigner logs `session_end` and deletes its checkpoint. ## Resharing Replaces shares, optionally with a new committee or threshold. Public key and addresses don't change. 1. Old committee and threshold come from the keygen record, not the caller. 2. At least `threshold + 1` old members must be online. 3. Old and new members run tss-lib resharing. New shares are staged; old ones stay active. 4. All participants report the same result and an unchanged public key. 5. Orchestrator persists the decision, then sends a signed `reshare_commit`. Each participant swaps in the new share. Abort before commit keeps the old shares. After commit it only rolls forward: the commit is retried until every participant confirms, including after a crash. > **Note:** Resharing is enabled per deployment. Contact Fystack before changing a committee. ## Failure codes | Code | Meaning | | --- | --- | | `CALLBACK_REJECTED` | Callback rejected, timed out or returned an invalid response. | | `APPROVAL_REQUIRED` | Wallet approver's signed approval missing. | | `UNAVAILABLE` | A participant was offline at submission. | | `PARTICIPANT_FAILED` | A participant reported a failure. See `failure_reason`. | | `PARTICIPANT_DISCONNECTED` | A participant's stream dropped mid-session. | | `SESSION_TIMEOUT` | Not done within 120 seconds. | | `RESULT_HASH_MISMATCH` | Participants disagreed on the result. | | `CONFLICT` | Another session is running for this wallet and protocol. | --- # Approval Callback Before joining any session, the cosigner sends a signed request to your server and waits for a signed `approve`. Your server gets the raw bytes to be signed, so it can verify the transaction independently of Fystack. _Diagram: Cosigner sends a signed request to the callback, which returns approve or reject_ ## How it works 1. Cosigner verifies the orchestrator's `session_start`. 2. Cosigner `POST`s an RS256 JWT to your URL (`Content-Type: application/jwt`). 3. Your server verifies it, runs policy, returns `200` with an RS256 JWT. 4. Cosigner verifies the response and the `request_id`, then joins the session. Requests are sent for `KEYGEN`, `SIGN` and `RESHARE`. Apply transaction policy to `SIGN`. > **Note:** The cosigner fails closed. Reject, non-200, redirect, timeout, oversized body, bad signature or wrong `request_id` all mean no signature. The session fails with `CALLBACK_REJECTED`. ## Setup Two RSA-4096 key pairs. Only public keys are exchanged. - **Cosigner key**: Created by `cosigner-cli`, signs requests. Give `cosigner-public.pem` to your server. - **Server key**: Created on your server or in KMS, signs responses. Give `server-public.pem` to the cosigner. **Same host:** point at the server's key folder. The CLI reads `server-public.pem` and writes `cosigner-public.pem`. Default URL is `http://host.docker.internal:8089/cosigner/approve`. ```bash cosigner-cli callback add ./node-callback-server/keys ``` **Remote server:** ```bash cosigner-cli callback add https://risk.example.com/cosigner/approve \ --server-public-key ./server-public.pem ``` The CLI prints the path and fingerprint of `cosigner-public.pem`; install it on your server. Add `--ca ./ca.pem` for a private CA. `callback add` updates `config.yaml`, sets `require_callback: true` and restarts the cosigner (skip with `--no-restart`). `cosigner-cli info` shows the current URL and fingerprint. ```yaml approval: require_callback: true callback: url: https://risk.example.com/cosigner/approve timeout: 30s retry_count: 1 private_key: type: file config: path: /run/secrets/cosigner-private.der.hex response_public_key_path: /run/secrets/server-public.pem # ca_cert_path: /run/secrets/callback-ca.pem ``` - `approval.require_callback` (boolean): Refuse all sessions without a callback. Keep `true` in production. - `callback.url` (string): HTTPS only. Plain HTTP allowed for `host.docker.internal`. `/cosigner/approve` is appended if no path. - `callback.timeout` (duration): Total budget including retry. Also the request JWT expiry. Default `30s`. - `callback.retry_count` (integer): Retries on network errors or `5xx`, same token. Max and default `1`. - `callback.private_key` (secret provider): Cosigner RSA-4096 signing key, hex PKCS#8 DER. - `callback.response_public_key_path` (string): Your server's RSA-4096 public key (PEM). - `callback.ca_cert_path` (string): CA certificate for a private or self-signed server certificate. ## Request JWT header `{"alg": "RS256", "typ": "JWT"}`. Accept only `RS256` and verify against `cosigner-public.pem` before reading claims. - `request_id` (string (UUID)): Unique per request, also `jti`. Echo it back. Use as idempotency key. - `iat, exp` (number): `exp` = `iat` + timeout. - `cosigner_id, workspace_id, participant_id` (string): Cosigner identity. - `session_id, orchestrator_id` (string): Session and the orchestrator that started it. - `coordinator_sequence, coordinator_signature` (number, string): The already-verified orchestrator signature. Log for traceability. - `protocol` (string): `ECDSA` or `EdDSA`. - `operation` (string): `KEYGEN`, `SIGN`, `RESHARE` or `PING`. - `threshold, participants` (number, array): Each participant's `participant_id`, `party_key`, `identity_public_key`. - `key_id` (string): Wallet key. - `signing_input` (string (base64)): `SIGN` only. Exact bytes to be signed. - `signing_input_sha256` (string (hex)): `SIGN` only. SHA-256 of `signing_input`. - `signing_context` (object): `SIGN` only, optional. Transaction details from Fystack, max 64 KiB. - `derivation` (object): `SIGN` only, optional. ECDSA non-hardened `path` and `delta`. - `new_threshold, new_participants` (number, array): `RESHARE` only. ```json { "request_id": "3f1c2a8e-7b0d-4d0c-9a51-2f6c4f0e9b12", "operation": "SIGN", "protocol": "ECDSA", "key_id": "8b0f3c4e-...", "signing_input": "q83vEjRWeJA...", "signing_input_sha256": "5d41402abc4b2a76...", "signing_context": { "version": 1, "apex_request_id": "wd_01J9...", "reference": { "type": "onchain_tx", "id": "..." }, "requested_at": "2026-10-12T08:30:00Z", "chain": { "network": "ethereum", "chain_id": "1", "protocol": "ECDSA" }, "wallet": { "wallet_id": "8b0f3c4e-...", "derivation_path": [44, 60, 0, 0, 0] }, "tx": { "raw": "0x02f8...", "payload_hash": "0x5d41402a...", "verified": true, "verification_mode": "evm_transaction", "decoded": { "to_address": "0xAbC...", "value": "1000000000000000000" } } }, "threshold": 1, "participants": [ ... ] } ``` `verification_mode`: `evm_transaction`, `solana_message`, `tron_transaction`, `bitcoin_transaction` or `digest_only`. > **Note:** `signing_context` is a hint, not proof. The cosigner does not check it against `signing_input`. Decode `tx.raw` yourself, recompute the hash, compare with `signing_input`, then check destination, amount and risk. Reject what you can't verify. ## Response HTTP `200`, `Content-Type: application/jwt`, RS256 with your server key. Max 64 KiB. Redirects are not followed. - `request_id` (string): Must match the request. - `action` (string): `approve` or `reject`. `pong` for `PING`. - `reason` (string): Required with `reject`, max 1024 bytes, omitted with `approve`. Written to the audit log. - `exp` (number): Required. Keep short, e.g. 60s. ```json { "request_id": "3f1c2a8e-7b0d-4d0c-9a51-2f6c4f0e9b12", "action": "reject", "reason": "destination_not_allowlisted", "iat": 1791794400, "exp": 1791794460, "jti": "b7e5..." } ``` ## Liveness The cosigner sends `"operation": "PING"` at startup and every 30 seconds. Reply `"action": "pong"` without running policy. - Startup ping fails: the cosigner stops. - Later ping fails: health shows `callback_unavailable` and signing is rejected until it recovers. ## Example server `cosigner-cli` ships a Node.js example in `examples/node-callback-server` (`express` + `jose`). ```js app.post('/cosigner/approve', express.text({ type: 'application/jwt' }), async (req, res) => { try { // RS256 only, signed by the pinned cosigner key, with exp and request_id. const request = await verifier.verify(req.body.trim()) if (request.operation === 'PING') { return res.type('application/jwt').send(await signer.sign(request, { action: 'pong' })) } let decision = { action: 'approve' } if (request.operation === 'SIGN' && !(await policyAllows(request))) { decision = { action: 'reject', reason: 'destination_not_allowlisted' } } res.type('application/jwt').send(await signer.sign(request, decision)) } catch (err) { res.sendStatus(401) } }) ``` > **Note:** As shipped, the example approves everything. Replace the decision logic before using real funds. ## Production checklist - Keep `require_callback: true`. `cosigner-cli callback remove` disables approval entirely. - Default deny. - Decode `tx.raw` and match it to `signing_input`. - Enforce allowlists, limits and AML checks. - Be idempotent by `request_id`; retries resend the same token. - Keep the server key in KMS or an HSM. AWS KMS `RSA_4096` with `RSASSA_PKCS1_V1_5_SHA_256` works for RS256. - Respond well within the timeout. - Alert on `callback_unavailable` and on rejections. --- # Installing the Cosigner Regular mode on Linux with Docker. [Nitro Enclave](https://docs.fystack.io/cosigner/nitro-enclave.md) support is coming soon. _Diagram: Install, verify, pair, connect_ ## Prerequisites - **Host**: Linux `amd64` or `arm64` for production. macOS or WSL 2 for development. - **Docker**: Engine 20.10+ with Compose v2. - **cosign**: v3+. The installer adds it if missing. - **From Fystack**: a pairing token (`cps_...`) and your Apex endpoint. - **Callback server**: required for production. See [Approval Callback](https://docs.fystack.io/cosigner/approval-callback.md). **Network:** no inbound ports. Outbound HTTPS to Apex, the Fystack Gateway, `ghcr.io`, your callback and (unattended profile) AWS KMS. --- ## Step 1: Install cosigner-cli ```bash curl -sSL https://cosigner.fystack.io/install.sh | sh ``` Checks the binary's SHA-256 and Sigstore signature, then installs to `/usr/local/bin` (or `~/.local/bin`). | Option | Env var | Effect | | --- | --- | --- | | `--version vX.Y.Z` | `COSIGNER_VERSION` | Install a specific release | | `--dir DIR` | `COSIGNER_INSTALL_DIR` | Install location | | `--skip-docker-check` | | Skip Docker checks | > **Note:** To verify the installer before running it, see [Supply chain verification](https://docs.fystack.io/cosigner/security.md#supply-chain-verification). ## Step 2: Create the instance directory ```bash export COSIGNER_HOME=/srv/cosigner mkdir -p $COSIGNER_HOME/secrets && chmod 700 $COSIGNER_HOME/secrets ``` ## Step 3: Write the config Pick a profile: - **`manual`** (default): storage key wrapped with a password. Enter it on every start. No auto-restart. - **`unattended`**: storage key wrapped by your AWS KMS key. Starts on boot. Keep the file outside the instance directory; `up` copies it in. **config.yaml** ```yaml storage: backend: sqlite sqlite: path: data/cosigner.db encryption: kek: type: password_envelope config: path: /run/secrets/storage-kek.enc approval: require_callback: true audit: type: file config: path: /var/lib/cosigner/audit.jsonl callback: url: https://YOUR-CALLBACK-HOST/cosigner/approve timeout: 30s retry_count: 1 private_key: type: file config: path: /run/secrets/cosigner-private.der.hex response_public_key_path: /run/secrets/server-public.pem ``` ```yaml storage: backend: sqlite sqlite: path: data/cosigner.db encryption: kek: type: kms config: provider: aws region: YOUR-AWS-REGION key_id: YOUR-KMS-KEY-ARN ciphertext_blob_path: /run/secrets/storage-kek.kms approval: require_callback: true audit: type: file config: path: /var/lib/cosigner/audit.jsonl callback: url: https://YOUR-CALLBACK-HOST/cosigner/approve timeout: 30s retry_count: 1 private_key: type: file config: path: /run/secrets/cosigner-private.der.hex response_public_key_path: /run/secrets/server-public.pem ``` - `/var/lib/cosigner` maps to `$COSIGNER_HOME/state`; `/run/secrets` maps to `$COSIGNER_HOME/secrets` (read-only). - **Unattended**: encrypt a random 32-byte key with your KMS key, save the base64 ciphertext as `secrets/storage-kek.kms`. The container needs `kms:Decrypt` via an IAM role. - **Callback keys**: place them in `secrets/` (mode `600`), or run `cosigner-cli callback add` after setup. > **Note:** Without `--config`, `up` writes a dev-only default with `require_callback: false`. Always pass a config in production. ## Step 4: Check the host ```bash cosigner-cli doctor ``` Checks Docker, cosign and config. Reports `unconfigured`, `configured but unpaired` or `paired`. ## Step 5: Pair and start ```bash cosigner-cli up \ --apex-endpoint https://YOUR-APEX-HOST \ --config /secure/config.yaml ``` Add `--profile unattended` for KMS. `up` then: 1. **Verifies** the newest `ghcr.io/fystack/cosigner` release with cosign and pins its digest, before asking for any secret. 2. **Prompts** for the storage password (manual) and pairing token. Use `--token-file` to pass the token non-interactively. 3. **Pairs**: generates an Ed25519 identity, registers it with Apex, saves `state/pairing.yaml`. 4. **Starts** the container, unlocks it and connects to the Gateway. Pin a release with `--image ghcr.io/fystack/cosigner@sha256:...`. Digests are on [cosigner.fystack.io](https://cosigner.fystack.io). ## Step 6: Verify ```bash cosigner-cli status cosigner-cli info cosigner-cli logs -f ``` Once `status` shows ready, the cosigner is online and can join keygen. ## Instance layout | Path | Contents | | --- | --- | | `cosigner-cli.yaml` | Apex endpoint, pinned image digest, profile | | `config.yaml` | Runtime config (read-only) | | `docker-compose.yaml` | Generated, hardened Compose file | | `secrets/` | Wrapped storage key and callback keys | | `state/pairing.yaml` | Pairing record from Apex | | `state/cosigner.key` | Ed25519 identity key | | `state/data/cosigner.db` | Encrypted key shares and session state | | `state/audit.jsonl` | Signed audit log | ## Run on boot Unattended profile only. Adapt the bundled systemd unit: ```ini [Unit] Description=Fystack Cosigner (KMS unattended profile) Requires=docker.service After=docker.service network-online.target Wants=network-online.target [Service] Type=oneshot RemainAfterExit=yes User=cosigner Group=cosigner SupplementaryGroups=docker ExecStart=/usr/local/bin/cosigner-cli --home /var/lib/fystack-cosigner start ExecStop=/usr/local/bin/cosigner-cli --home /var/lib/fystack-cosigner stop TimeoutStartSec=360 TimeoutStopSec=60 UMask=0077 [Install] WantedBy=multi-user.target ``` For the manual profile, pass the password through a file descriptor: ```bash COSIGNER_PASSWORD_FD=3 cosigner-cli start 3< /run/secrets/cosigner-pw ``` ## Troubleshooting | Symptom | Fix | | --- | --- | | Image signature verification failed | Don't bypass. Check the image reference and contact security@fystack.io. | | identity already registered | Restore the original pairing data from backup instead of re-pairing. | | Exits right after start | Startup callback ping failed. Check the callback is reachable and trusts `cosigner-public.pem`. | | AlreadyExists | Another instance with this identity is connected. Stop it. | | PermissionDenied | Cosigner was revoked. It will not reconnect. | | Not ready after first start | Generating ECDSA pre-parameters. Wait up to 5 minutes. | --- # Cosigner Operations CLI reference, backups, updates and monitoring. ## Commands Global flags: `--home` (instance directory, default `COSIGNER_HOME` then current directory) and `--docker`. | Command | Does | Flags | | --- | --- | --- | | `up` | Verify, pair and start a new instance. Same as `start` once paired. | `--apex-endpoint` `--config` `--profile` `--image` `--token-file` | | `start` | Start. Asks for the password in manual profile. | | | `stop` | Stop, keep state. Drains sessions for up to 25s. | | | `restart` | Recreate the container. | | | `status` | `ready`, `state`, `participant_id`, `active_sessions`. | | | `logs` | Container logs. | `--tail` `-f` | | `doctor` | Check host, config and image signature. | | | `info` | Public key, image, callback fingerprint. | | | `callback add` | Require callback approval. | `--url` `--server-public-key` `--ca` `--no-restart` | | `callback remove` | Stop requiring approval. | `-y` `--no-restart` | | `backup` | Encrypted instance backup. Cosigner must be stopped. | `--out` | | `restore` | Restore into an empty directory. | `--in` | | `export` | Write `.mshare` recovery files. Manual profile, stopped. | `--wallet` `--out-dir` `--list` | | `update` | Verify and install a newer release after a backup. | `--version` `--backup-out` `--dry-run` | | `version` | CLI version. | | ### Environment variables | Variable | Use | | --- | --- | | `COSIGNER_HOME` | Instance directory | | `COSIGNER_PASSWORD_FD` | Read the storage password from a file descriptor | | `COSIGNER_RECOVERY_PASSWORD_FD` | Read the backup password from a file descriptor | | `COSIGNER_NO_UPDATE_CHECK=1` | Hide update notices | ## Profiles | | manual | unattended | | --- | --- | --- | | Storage key | Password (Argon2id) | AWS KMS | | Start | Password every time | Automatic, on boot | | Docker restart | `no` | `on-failure:5` | | `.mshare` export | Yes | No | Set at `up`. Cannot be changed later. ## Backup and restore `backup` writes one archive with config, secrets, identity, pairing record, encrypted database and audit log. It is encrypted with a separate recovery password (Argon2id + AES-256-GCM). ```bash cosigner-cli stop cosigner-cli backup --out /backups/cosigner-2026-10-12.cosrec cosigner-cli start ``` Write it outside the instance directory. Instance data is limited to 128 MiB. ```bash cosigner-cli --home /srv/cosigner-restored restore --in /backups/cosigner-2026-10-12.cosrec cosigner-cli --home /srv/cosigner-restored start ``` > **Note:** Stop or revoke the old instance first. Never run two copies of the same cosigner. Test restores on a clean host regularly. ## Recovery export `export` writes one `.mshare` per wallet, encrypted with a six-word passphrase printed once. These are the cosigner's input to Fystack Recovery, which rebuilds keys offline from a threshold of shares. One file alone cannot rebuild a key. ```bash cosigner-cli stop cosigner-cli export --out-dir /secure/recovery cosigner-cli start ``` Store passphrases separately from the files. ## Updates ```bash cosigner-cli update --dry-run cosigner-cli update --backup-out /backups/pre-update.cosrec ``` 1. Picks the newest release (or `--version`). No downgrades. 2. Verifies the signature and container contract. 3. Stops the cosigner and writes a backup. 4. Pins the new digest and starts. No automatic rollback. Keep the backup until the new version has signed real transactions. ## Monitoring **Health**: checked every 10s; stale after 15s. Alert on: - `ready: false` beyond first-boot setup. - `state: callback_unavailable`. - Container exit (manual profile never restarts on its own). **Audit log**: `state/audit.jsonl`, signed and hash-chained. Events: `session_start`, `session_end`, `session_abort`, `callback_decision`, `kek_use`, `identity_sign`, `backup_created`, `restore_performed`. Ship it off-host; Docker does not rotate it. **Logs**: Docker `json-file`, 10 MB x 5. ## Decommissioning 1. Ask Fystack to revoke the cosigner. 2. `cosigner-cli stop`. 3. Keep a final backup and exports per your retention policy, then delete the instance directory. --- # Nitro Enclave Deployment Coming soon Running the cosigner inside an [AWS Nitro Enclave](https://aws.amazon.com/ec2/nitro/nitro-enclaves/) is in testing and not yet supported for production. Use [regular mode](https://docs.fystack.io/cosigner/installation.md) today. ## What's planned _Diagram: Planned Nitro deployment: enclave and host on one EC2 instance linked by vsock, with the host relaying to the Fystack Gateway, DynamoDB and AWS KMS_ - **Enclave**: runs MPC, holds the identity key and decrypts key shares. No network, disk or shell. - **Host**: untrusted relay for the Gateway stream, storage and KMS. - **Attested key release**: AWS KMS releases the storage key only to an enclave whose PCR0 matches your key policy. Result: host admins cannot read key shares. > **Note:** Need enclave isolation now? Contact Fystack to join early testing. --- # Cosigner Security Model No single party can move funds alone: not Fystack, not your host, not anything in between. ## Who controls what | You | Fystack | | --- | --- | | Cosigner host, storage, network | Apex: pairing, identities, revocation | | Storage key: password or KMS policy | Orchestrator: sessions | | Callback server, policy and keys | Gateway: stream auth and relay | | Backups, exports, upgrades | MPCium nodes and workspace policies | ## Signing controls Every signature passes all of these: 1. **Workspace policies** and [approval groups](https://docs.fystack.io/product/approval-group.md) in Apex. 2. **Initiator signature**: orchestrator accepts only Apex-signed requests. 3. **Committee pinning**: participants and threshold must match keygen. 4. **Orchestrator signature**: cosigner verifies it against the key pinned at pairing. 5. **Approver signature**: required for wallets with an approver. 6. **Your callback**: must return a signed `approve`. 7. **Threshold**: your cosigner's share is required. If Fystack were fully compromised, 1 to 5 could fail. Step 6 still holds: it runs on your infrastructure, with keys Fystack never sees, against the raw bytes being signed. ## Authentication **Cosigner identity**: Ed25519 key. Stored at `state/cosigner.key` (`0600`). Registered with Apex once via a pairing token. It signs the Gateway JWT, every session event and peer message, and audit entries. **Cosigner → Fystack**: EdDSA JWT per stream. ```json { "iss": "", "sub": "", "aud": "cosigner-gateway", "ctx": "cosigner-gateway-auth-v1", "jti": "", "iat": 1791794400, "exp": 1791794460 } ``` The Gateway accepts only `EdDSA`, checks audience and expiry (30s skew) and rejects reused `jti` for 2 minutes. Revocation disconnects live streams immediately. **Fystack → cosigner**: The orchestrator key and fingerprint are pinned at pairing. Every control message must be signed by it with an increasing sequence number. Result signatures use a separate prefix so they can't be replayed as control messages. **Participant ↔ participant**: Peer keys arrive in the signed `session_start`. Signed X25519 exchange, per-direction HKDF keys, ChaCha20-Poly1305. Gateway and orchestrator see only ciphertext. ## Encryption at rest _Diagram: Password or AWS KMS unwraps the storage KEK, HKDF derives four scope keys, each row is AES-256-GCM_ - **Storage key**: 32 bytes, unwrapped at start from a password (Argon2id `t=3`, 64 MiB) or AWS KMS. Memory only, zeroed after use. - **Scope keys**: HKDF-SHA256 derives separate keys for `shares`, `preparams`, `checkpoints`, `metadata`. - **Rows**: AES-256-GCM, random 12-byte nonce, bound to scope and row key. Moved rows fail to decrypt. - **No plaintext shares leave**: stripped from events. Exports are sealed to RSA-OAEP + AES-GCM or a passphrase. **Password unlock**: `cosigner-cli` never puts the password in env or args. It opens a one-shot local socket; the container proves a 32-byte token via HMAC and receives the password AES-GCM encrypted. ## Runtime hardening - Distroless, non-root image pinned by digest. - Read-only root filesystem; `tmpfs` for `/tmp` and `/run/cosigner`. - All capabilities dropped, `no-new-privileges`. - Config and secrets mounted read-only. - Limits: 128 KiB control messages, 20 starts and 10 aborts per minute, 5 concurrent sessions. ## Audit trail Append-only JSONL. Each entry is signed and includes the previous entry's hash, so edits and deletions are detectable. Ship it off-host. ## Supply chain verification Binaries, installers and images are built by GitHub Actions release workflows and signed with Sigstore. The certificate names the exact workflow and tag. The runtime pipeline also runs `go mod verify`, race tests, a reproducibility check, `govulncheck` and `gosec`, and attaches an SBOM and provenance. `cosigner-cli` verifies on every `up`, `update` and `doctor`. To check by hand: **Verify release artifacts** ```bash cosign verify ghcr.io/fystack/cosigner@sha256: \ --certificate-identity https://github.com/fystack/cosigner/.github/workflows/release.yml@refs/tags/ \ --certificate-oidc-issuer https://token.actions.githubusercontent.com ``` ```bash curl -sSLO https://cosigner.fystack.io//install.sh curl -sSLO https://cosigner.fystack.io//install.sh.sigstore.json cosign verify-blob install.sh \ --bundle install.sh.sigstore.json \ --certificate-identity https://github.com/fystack/cosigner-cli/.github/workflows/release.yml@refs/tags/ \ --certificate-oidc-issuer https://token.actions.githubusercontent.com ``` Digests and checksums: [cosigner.fystack.io](https://cosigner.fystack.io/#verify). Report mismatches to security@fystack.io. ## Residual risks - **Regular mode = host custody.** Root or Docker access on the host can read memory. [Nitro Enclave](https://docs.fystack.io/cosigner/nitro-enclave.md) support (coming soon) will remove that. - **Callback quality.** A callback that approves without decoding the transaction adds nothing. Default deny. - **Availability.** A hostile host can block traffic or delete and roll back rows. Encryption detects edits, not deletion. Keep backups. - **Duplicates.** Never run the same cosigner on two hosts. - **Recovery material.** Backup passwords and export passphrases unlock your share. Store them separately. Before real funds: test restore, restart, failure and IAM revocation, and get an independent review. --- # Security An overview of how Fystack protects private keys, data, and infrastructure at every layer, from encryption at rest to deployment hardening. - [Encryption at Rest](https://docs.fystack.io/security/encryption-at-rest.md): How Fystack protects private keys, seeds, and secrets by encrypting every asset at rest with AES-256-GCM and layered key management. - [Data Tampering Detection](https://docs.fystack.io/security/database-integrity.md): How Fystack detects database tampering with Ed25519 signatures, SHA-256 hashing, and automated verification to keep wallet data trustworthy. - [In Memory Runtime encryption](https://docs.fystack.io/security/runtime-key-encryption.md): How Fystack protects sensitive credentials in memory using guarded buffers, zeroization, and runtime policies to prevent memory-based attacks. - [Secret Store & KMS Integration](https://docs.fystack.io/security/secret-store-kms-integration.md): How Fystack integrates with enterprise-grade secret stores and key management systems to securely manage credentials and signing keys. - [MPC Wallet Security](https://docs.fystack.io/security/mpc-security.md): How Fystack implements Multi-Party Computation (MPC) wallet security with threshold signatures, distributed key generation, and secure signing. - [Supply Chain Security](https://docs.fystack.io/security/supply-chain-security.md): How Fystack verifies dependencies, build artifacts, and deployment pipelines to prevent supply chain attacks. - [Container Security & Build Provenance](https://docs.fystack.io/security/docker-build-hardening.md): How Fystack ensures verifiable, tamper-resistant container builds through SLSA-compliant provenance and keyless cryptographic signing. - [Deployment Best Practices](https://docs.fystack.io/security/deployment-best-practices.md): How Fystack implements secure deployment practices with private network isolation, strict IAM controls, and endpoint detection. --- # Encryption at Rest Fystack implements comprehensive encryption at rest using a KEK/DEK (Key Encryption Key / Data Encryption Key) architecture. Every sensitive payload is encrypted before it ever touches disk. Data Encryption Keys (DEKs) are used to encrypt individual records or blobs, while the Key Encryption Key (KEK), securely stored in a vault such as AWS Secrets Manager is used to wrap and protect the DEKs. This ensures that even in the event of full database or backup exfiltration, only ciphertext is exposed, preserving confidentiality end-to-end. The flow mirrors industry security standards, meeting FIPS 197 and NIST SP 800-38D requirements for AES-GCM and satisfying PCI DSS and GDPR Article 32 mandates for protecting sensitive data at rest. _Image: KEK/DEK flow_ > New to KEK/DEK layering? Read our blog primer: [DEK/KEK: The industry standard to protect highly sensitive data (Part 1)](https://fystack.io/blog/dek-kek-the-industry-standard-to-protect-highly-sensitive-data-part-1). ## 1. Overview Every sensitive value is encrypted using AES-256-GCM and stored inside a versioned envelope that tracks algorithm choices and key versions. Workspace isolation, per-chain derivation, and vault-backed master keys combine to keep data sealed even if infrastructure is breached. ### Why AES-256-GCM? 1. **NIST Approved**: Federal encryption standard (FIPS 197). 2. **Authenticated Encryption**: Simultaneous confidentiality and integrity. 3. **Performance**: Hardware acceleration via AES-NI on modern CPUs. 4. **Industry Standard**: Used by AWS, Google Cloud, Azure for data-at-rest. ## 2. What Data Is Encrypted - **Private keys and seeds:** All wallet material for [Hyper wallets](https://docs.fystack.io/wallets.md#hyper-wallet) is encrypted before storage using workspace-specific DEKs. | Component | Description | | --- | --- | | Global Encryption Key | 32-byte AES key loaded dynamically from AWS Secrets Manager, with fallbacks to environment variables or secret managers such as HashiCorp Vault or Doppler. | | Workspace Seed | Random 32-byte seed generated uniquely per workspace. | | Encryption Algorithm | AES-GCM (authenticated encryption) protects every ciphertext. | | Key Derivation | Each workspace key is derived using HKDF(global_encryption_key, salt). | | Wallet Derivation | BIP44 for EVM chains and Tron; SLIP-10 for modern chains like Solana and Aptos. | _Image: image-2025-11-07T15-53-24-429Z_ - **User credentials:** TOTP secrets and other authentication artifacts ride the same DEK/KEK architecture to remain sealed at rest. _Image: image-2025-11-07T15-51-02-776Z_ ### Envelope Format Each ciphertext includes the algorithm identifier, version metadata, derivation context, and payload. This enables algorithm agility, backward compatibility, and full provenance for every encrypted record. --- # Data Tampering Detection Fystack hardens its operational databases with a parallel integrity layer that cryptographically signs sensitive rows. Each protected record produces a SHA-256 hash that is signed with an Ed25519 key stored in a dedicated SecretStore. Signatures live in their own integrity table, so even if an attacker alters operational data, the tampering is immediately detectable. > **Info.** This change helps prevent insider attacks from gaining access to the database and modifying records in stealth, ensuring any unauthorized change is instantly detected and provably tamper-evident. _Image: image-2025-11-09T07-02-02-296Z_ _Image: Withdrawal blocked due to tampered wallet data_ Withdrawal is blocked due to tampered wallet data ## 1. Integrity Workflow ### How the integrity layer works? _Image: image-2025-11-09T08-04-22-396Z_ - **Deterministic serialization:** Protected fields are sorted and normalized before hashing, guaranteeing the same byte stream for the same logical data. ### What gets protected - **Wallet metadata:** Wallet ID, workspace ownership, user ownership, threshold settings. - **Wallet public keys:** Public key contents, algorithm family (ECDSA/EdDSA), wallet association. - **Wallet settings:** Auto-approval limits, spending policies, risk controls. - **Wallet addresses:** On-chain address, address type, network standards. - **HD key metadata:** Derivation indices, encryption versions, blockchain flavor. - **MPC wallet configuration:** Signature thresholds (2-of-3, etc.), MPC library versions, TSS protocol revisions. ### Signing & verification lifecycle 1. Extract the fields marked for integrity coverage. 2. Serialize deterministically so field order and formatting never drift. 3. Hash with SHA-256 to produce a compact fingerprint. 4. Sign the hash with an Ed25519 private key held in SecretStore or KMS. 5. Store the signature, hash, version, and metadata in the integrity records table. ### Verification 1. Fetch the current operational record when it is read or audited. 2. Recompute the SHA-256 hash from the current field values. 3. Compare the recomputed hash to the stored hash; any mismatch flags tampering. 4. Verify the Ed25519 signature using the current public key version from configuration (not the database). 5. Block the operation, alert security, and log the attempt if either step fails. ## 2. Detection & Monitoring ### Modified wallet threshold - Attack: DBA attempts to change a wallet from 2-of-3 to 1-of-3. - Result: Hash mismatch → signature verification fails → wallet operations blocked → alert fired. ### Inflated auto-approval limit - Attack: Insider raises the auto-approval limit from $1,000 to $1,000,000. - Result: Signed limit field changes → system refuses to honor tampered value and logs incident. ### Substituted deposit address - Attack: Attacker swaps a deposit address for their own. - Result: Address hash differs → signature mismatch → payout halted → security notified. ### Threat protection matrix | Scenario | Protection | | --- | --- | | Compromised DB credentials | Tampered rows produce new hashes, so signatures fail verification immediately. | | Backup restoration with edited data | Restored records no longer match persisted signatures, triggering incident response. | | Malicious DBA changes policies | Auto-approval and policy fields are signed; operations block until security review. | | Compromised admin account edits wallet configs | Verification uses out-of-band public keys, so forged signatures are impossible. | --- # In-Memory Runtime Encryption ## 1. Overview When sensitive data such as API keys, access tokens, or private credentials are used by the application, they temporarily reside in heap memory. To reduce the chance of malware or memory scanners locating these secrets, we leverage a specialized in-memory protection layer built with [memguard](https://github.com/awnumar/memguard). This ensures sensitive information is stored in locked, guarded, and short-lived memory regions, preventing accidental exposure or persistence after use. > **Warning.** While in-memory runtime encryption significantly reduces the risk of side channel attacks, it does not fully eliminate them 100% and is not a full replacement for a fully TEE (Trusted Execution Environment) environment similar to Confidential VM. _Image: In-Memory Runtime Encryption: Protect Sensitive Keys and Credentials from Memory Scanners and Malware with Locked, Guarded Heap Memory._ In-Memory Runtime Encryption: Protect Sensitive Keys and Credentials from Memory Scanners and Malware with Locked, Guarded Heap Memory. ## 2. Why It Matters Even with encryption at rest and in transit, secrets can still be exposed while in use — when decrypted for runtime operations. Memory attacks often target this exact window by scanning the process heap for recognizable key patterns. By isolating credentials in guarded memory: - **Data is locked to physical RAM, not swapped to disk.** - **Memory pages are surrounded by guard regions that detect or block access attempts.** - **Buffers are explicitly wiped after use.** - **The secret's lifetime in memory is minimized to milliseconds or seconds.** This approach provides defense-in-depth against runtime credential harvesting and in-process malware. ## 3. Architecture Overview ### 3.1 Decryption and Loading Sensitive data (e.g., API keys or tokens) are decrypted from secure storage or retrieved from a secret manager, then placed directly into protected heap memory. ### 3.2 Guarded Memory Zone Once loaded, the data is kept in a memguard-protected enclave: - Non-swappable (mlock-based protection) - Surrounded by guard pages - Automatically destroyed on error or end of scope ### 3.3 Ephemeral Access The protected data is only accessed briefly during operations that require it (e.g., authentication, signing, or encryption). No permanent copies are created elsewhere in memory. ### 3.4 Immediate Zeroing After the operation, the memory buffer is securely wiped and released, leaving no recoverable traces of the secret. ## 4. Summary Sensitive credentials are decrypted only into guarded heap memory, isolated from standard process space. They are used briefly, wiped immediately, and never written to logs or disk — ensuring that even a compromised host or memory inspection attempt cannot recover secrets in use. --- # Secret Store & KMS Integration ## 1. Overview In addition to supporting environment-based configuration for credentials and API keys, Fystack integrates seamlessly with AWS infrastructure, including AWS Secrets Manager and AWS KMS — to ensure that **all credentials and signing keys remain securely managed within hardware-backed** cloud environments. Support for Google Cloud, Azure, and HashiCorp Vault is currently in development. _Image: Secret Store & KMS Integration_ Secret Store & KMS Integration ## 2. Supported Providers | Provider | Secrets Manager | Key Management Service (KMS) | Status | | --- | --- | --- | --- | | AWS | Supported | Supported | Production Ready | | Google Cloud | Under Development | Under Development | Coming Soon | | Azure | Under Development | Under Development | Coming Soon | | HashiCorp Vault | Under Development | Under Development | Coming Soon | --- # MPC Wallet Security Fystack uses **Multi-Party Computation (MPC)** to eliminate single points of failure in private key management. No single party ever possesses a complete private key. Instead, key shares are distributed across multiple nodes, and signatures are computed collaboratively using threshold cryptography. This document outlines how Fystack's MPC implementation protects wallet keys through distributed trust, secure communication, and verifiable computation. _Image: Fystack MPC wallet security architecture_ ### Node Responsibilities Each node: - Stores only **its encrypted share** locally. - Participates in threshold signing using **Ed25519 verification** and **ECDH-secured P2P** communication. - Cannot reconstruct or export the full key, even if compromised. | Property | Description | | --- | --- | | Confidentiality | No single node or operator can access a full private key. | | Integrity | All inter-node messages are authenticated via Ed25519 or Cloud KMS signatures. | | Resilience | Supports n nodes with threshold t, tolerating up to (t-1) node failures. | | Encryption at Rest | Key shares are stored locally with AES encryption. | | Encryption in Transit | Enforced via NATS TLS plus an ECDH layer for P2P channels. | --- ## 1. Overview Multi-Party Computation enables multiple parties to jointly compute cryptographic operations without any single party having access to complete private keys. Fystack implements threshold signature schemes where a minimum number of parties (threshold) must cooperate to produce a valid signature. | Component | Description | | --- | --- | | MPC Nodes | Minimum of 3 nodes form a cluster. | | Threshold (t) | Wallet remains secure even if up to (t-1) nodes are compromised. | | Key Shares | Private key is mathematically split into multiple shares. | | Share Encryption | Each share is encrypted using AES (BadgerDB default encryption). | | Messaging Security | Dual-layer protection: NATS TLS plus ECDH key exchange for P2P encryption. | | Message Authentication | Apex communicates with the MPC cluster using signed messages (Ed25519 or Cloud KMS). | ### Key Benefits - **No single point of compromise:** Compromise of one node does not expose private keys - **Distributed trust:** Multiple independent parties must cooperate - **Transparent to blockchain:** Output signatures are standard ECDSA/EdDSA signatures - **Dynamic threshold:** Can adjust signing requirements based on transaction value - **Audit trail:** Every signing session is logged and traceable --- ## 2. Distributed Key Generation (DKG) Fystack uses **Distributed Key Generation** to create wallet keys without any party ever possessing the complete private key. ### DKG Process **Phase 1: Commitment** - Each party generates a random polynomial of degree t-1 (where t is the threshold) - Parties broadcast commitments to their polynomials - Commitments are verified using Feldman's VSS (Verifiable Secret Sharing) **Phase 2: Share Distribution** - Each party evaluates their polynomial at specific points - Shares are sent securely to corresponding parties over encrypted channels - Each party receives one share from every other party **Phase 3: Verification** - Parties verify received shares against broadcast commitments - Invalid shares are identified and complaints are filed - Protocol aborts if too many invalid shares are detected **Phase 4: Key Construction** - Each party combines valid shares to form their key share - Public key is computed from the commitments - No party knows the complete private key **DKG Configuration** ```yaml mpc_threshold: 2 # Number of MPC nodes required to sign ``` `mpc_threshold` represents the threshold `t` in an n-of-t scheme. With `n` active MPC nodes, the system enforces `2 ≤ t ≤ n`, tolerating up to `(t - 1)` faulty or offline nodes while still producing signatures. For example, `n = 3` with `mpc_threshold: 2` yields a 2-of-3 cluster: any two authenticated nodes can sign, but compromising a single node is insufficient to recover the private key. For GG18/GG20-style robust MPC protocols we additionally recommend `t > n / 2`, which guarantees Byzantine fault tolerance: even if `f = n - t` nodes are malicious, offline, or colluding, honest nodes remain a majority and can prevent bogus signatures or private-key reconstruction. ### Security Properties - **Robustness:** Tolerates up to n-t malicious parties - **Verifiability:** All parties can verify correctness of shares - **Privacy:** No information about private key is leaked - **Non-interactive:** After setup, no further DKG required --- ## 3. Threshold Signing Threshold signing allows a subset of parties (meeting the threshold) to collaboratively produce a valid signature without reconstructing the private key. ### Signing Protocol (ECDSA) **Round 1: Commitment Phase** **Commitment Generation** ```text // Each signer generates commitment const k_i = randomScalar() const gamma_i = randomScalar() const commitment_i = { K_i: k_i * G, // Ephemeral public key Gamma_i: gamma_i * G, // Blinding factor } broadcastCommitment(commitment_i) ``` **Round 2: Partial Signature Generation** **Partial Signature** ```text // After receiving all commitments const R = combineCommitments(allCommitments) const r = R.x // x-coordinate of combined point // Each party computes partial signature const s_i = k_i + r * keyShare_i * hash(message) broadcastPartialSignature(s_i) ``` **Round 3: Signature Aggregation** **Signature Combination** ```text // Combine partial signatures const s = lagrangeInterpolation(partialSignatures) const signature = { r, s } // Verify signature const valid = ecdsaVerify(signature, publicKey, message) ``` ### Signing Workflow | Step | Action | Security Check | | --- | --- | --- | | 1. Session Init | Create signing session with transaction details | Verify session ID uniqueness | | 2. Party Selection | Select threshold number of parties to participate | Verify parties are online and authenticated | | 3. Commitment Round | Exchange commitments to ephemeral keys | Validate commitment format and freshness | | 4. Partial Signatures | Each party computes and shares partial signature | Verify partial signature validity | | 5. Aggregation | Combine partial signatures into final signature | Verify final signature against public key | | 6. Broadcast | Submit signed transaction to blockchain | Confirm transaction acceptance | --- ## 4. Network Security ### NATS Transport (TLS) MPC nodes exchange traffic over the shared NATS bus declared in `config.yaml`. Production deployments require TLS endpoints plus pinned client certificates: **NATS TLS configuration** ```yaml nats: url: tls://nats.example.com:4222 username: "" password: "" tls: client_cert: "/etc/mpcium/certs/client-cert.pem" client_key: "/etc/mpcium/certs/client-key.pem" ca_cert: "/etc/mpcium/certs/rootCA.pem" ``` The `deployments/systemd/setup-config.sh` script refuses to run in production without the `tls://` URL and the `nats.tls.*` paths above, ensuring every node speaks to NATS through an authenticated TLS tunnel. ### Signed Message Bus Before a TSS payload is published, the node signs it with its Ed25519 identity (see `pkg/identity/fileStore`): **Sign TSS messages** ```go func (s *fileStore) SignMessage(msg *types.TssMessage) ([]byte, error) { msgBytes, err := msg.MarshalForSigning() if err != nil { return nil, fmt.Errorf("failed to marshal message for signing: %w", err) } signature := ed25519.Sign(s.privateKey, msgBytes) return signature, nil } ``` Every node verifies the signature before accepting a message, preventing spoofed peers or replayed data: **Verify TSS messages** ```go func (s *fileStore) VerifyMessage(msg *types.TssMessage) error { if msg.Signature == nil { return fmt.Errorf("message has no signature") } publicKey, err := s.GetPublicKey(partyIDToNodeID(msg.From)) if err != nil { return fmt.Errorf("failed to get sender's public key: %w", err) } msgBytes, err := msg.MarshalForSigning() if err != nil { return fmt.Errorf("failed to marshal message for verification: %w", err) } if !ed25519.Verify(publicKey, msgBytes, msg.Signature) { return fmt.Errorf("invalid signature") } return nil } ``` ### Point-to-Point Encryption Nodes run an ECDH exchange over dedicated NATS topics (`pkg/mpc/key_exchange_session.go`) to derive symmetric keys per peer. Those keys drive AES-GCM encryption for every payload: **Encrypt TSS payloads** ```go func (s *fileStore) EncryptMessage(plaintext []byte, peerID string) ([]byte, error) { key, err := s.GetSymmetricKey(peerID) if err != nil { return nil, err } if key == nil { return nil, fmt.Errorf("no symmetric key for peer %s", peerID) } return encryption.EncryptAESGCMWithNonceEmbed(plaintext, key) } ``` ### ECDH Handshake Flow The ECDH session (`pkg/mpc/key_exchange_session.go`) is layered on top of the NATS bus: 1. Each node generates an ephemeral X25519 key pair and subscribes to the shared `ecdh:exchange` topic. 2. Nodes broadcast their public key plus timestamp, signed with their Ed25519 identity (`SignEcdhMessage`). 3. Listeners verify the signature, perform `ECDH()` using their own private key, and derive a symmetric key with HKDF-SHA256. 4. The derived 32-byte key is cached per peer via `identityStore.SetSymmetricKey` and reused by the AES-GCM helpers above. **HKDF key derivation** ```go func (e *ecdhSession) deriveSymmetricKey(secret []byte, peerID string) []byte { info := deriveConsistentInfo(e.nodeID, peerID) // stable ordering of IDs hkdfReader := hkdf.New(sha256.New, secret, nil, info) key := make([]byte, 32) // AES-256 key if _, err := hkdfReader.Read(key); err != nil { e.errCh <- err return nil } return key } ``` If a peer disconnects, `RemovePeer` drops the cached symmetric key so a fresh handshake is required on reconnect, keeping channel keys ephemeral. ### Network Isolation | Control | Implementation | | --- | --- | | Private Network | MPC nodes communicate over private VPC/VPN | | Firewall Rules | Only MPC ports open, restricted to known node IPs | | Rate Limiting | Maximum message rate per node to prevent DoS | | Connection Monitoring | All connections logged and monitored for anomalies | --- ## 5. Storage Security Key shares persist inside BadgerDB. The MPC service refuses to open the database unless an encryption key (`badger_password`) is supplied, and that key is fed into Badger's AES-256 encryption via `WithEncryptionKey(...)` (`pkg/kvstore/badger.go`). Backups reuse the same encrypted format, so both the live DB files and snapshot archives remain unreadable without the password. **Badger encryption** ```go opts := badger.DefaultOptions(config.DBPath). WithEncryptionKey(config.EncryptionKey). WithCompression(options.ZSTD) db, err := badger.Open(opts) ``` --- ## 6. Summary Fystack's MPC implementation eliminates single points of failure through distributed trust, threshold cryptography, and secure multi-party protocols. By splitting private keys into shares and requiring collaborative signing, we ensure that no single compromise can lead to asset loss. **Key security features:** - **Distributed Key Generation** with no single point of trust - **Threshold signatures** requiring multiple parties to cooperate - **Encrypted communication** with TLS + ECDH-derived AES-GCM payloads - **BadgerDB encryption at rest** using AES-256 with operator-supplied passwords - **Verifiable computation** to detect malicious parties --- # Supply Chain Security ## 1. Overview Fystack treats every dependency, artifact, and pipeline hop as a potential attack surface. Source pinning, SBOM validation, reproducible builds, and artifact signing help guarantee that what ships to production exactly matches audited source. ## 2. Static Application Security Testing (SAST) Fystack employs multiple Static Application Security Testing (SAST) tools to monitor supply chain and dependency security. These tools continuously analyze source code, dependencies, and build artifacts to identify potential vulnerabilities and security risks before they reach production. _Image: Fystack Code Scanning Security_ Fystack Code Scanning Security ## 3. Dependency Management Fystack minimizes usage of dependencies if unnecessary. By reducing the dependency footprint, we decrease the attack surface and potential vulnerability exposure. Each dependency is carefully evaluated for necessity, security posture, and maintenance status before being included in the project. ## 4. Supply Chain Vulnerability Monitoring Fystack uses a combination of tools to stay updated about supply chain vulnerabilities: - **GitHub Code Scanning:** Integrated into the development workflow to automatically scan code for security issues and vulnerabilities - **Socket:** Monitors dependencies for security vulnerabilities and suspicious package behavior in real-time - **Snyk:** Provides continuous monitoring and scanning of dependencies, containers, and infrastructure as code for known vulnerabilities These tools work together to provide comprehensive coverage of the supply chain, alerting the security team immediately when new vulnerabilities are discovered. ## 5. Layered CI/CD Pipeline Fystack has a layered CI/CD pipeline that checks code security and vulnerabilities for dependencies at multiple stages: - **Pre-commit checks:** Automated security scans before code is committed - **Build-time validation:** Dependency vulnerability scanning during the build process - **Pre-deployment gates:** Final security checks before artifacts are deployed - **Post-deployment monitoring:** Continuous monitoring of running applications Each layer provides defense-in-depth, ensuring that vulnerabilities are caught early and prevented from reaching production environments. ## 6. Best Practices Environment hardening, mandatory code review, and continuous dependency scanning ensure that the same security controls are maintained throughout the release process. All dependencies are pinned to specific versions, and SBOM (Software Bill of Materials) validation ensures complete transparency of what is included in each build. --- # Container Security and Build Provenance This document outlines how Fystack ensures verifiable, tamper-resistant container builds through **SLSA-compliant provenance**, **keyless cryptographic signing**, and **public transparency logs**. It allows us to **prove that every production container image originates from a specific Git commit** and has not been tampered with. --- ## 1. Overview Each production image is built using **Docker BuildKit** with embedded **SLSA Level 3 provenance** and **SPDX SBOM** metadata. All artifacts are **keylessly signed** using **Sigstore Cosign** and recorded in the **Rekor transparency log**, creating an immutable audit trail. **Integrity Chain:** **Integrity Chain** ``` Git Commit → SLSA Provenance → Cosign Signature → Rekor Transparency Log ``` ### Distroless Base Images Fystack uses **distroless images** to minimize attack vectors from the operating system. Distroless images contain only the application and its runtime dependencies, eliminating package managers, shells, and other utilities that could be exploited. _Image: Distroless container architecture_ **Distroless Dockerfile** ```bash FROM golang:1.25 AS builder ARG TARGETOS=linux ARG TARGETARCH=amd64 WORKDIR /src COPY go.mod go.sum ./ RUN --mount=type=cache,target=/go/pkg/mod \ go mod download COPY . . RUN --mount=type=cache,target=/go/pkg/mod \ --mount=type=cache,target=/root/.cache/go-build \ CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} \ go build -o /out/mpcium ./cmd/mpcium FROM gcr.io/distroless/base-debian12:latest USER nonroot:nonroot WORKDIR /app COPY --from=builder /out/mpcium /app/mpcium ENTRYPOINT ["/app/mpcium"] ``` This approach: - Reduces the attack surface by eliminating unnecessary OS packages - Runs as a non-root user for additional security - Uses BuildKit cache mounts for faster, more efficient builds - Produces minimal final images (typically <20MB for Go binaries) --- ## 2. Build Provenance & Signing _Image: Secure CI/CD Pipeline: SLSA Level 3 provenance with keyless signing and transparency logs_ Secure CI/CD Pipeline: SLSA Level 3 provenance with keyless signing and transparency logs ### SLSA Level 3 Provenance **Workflow:** `.github/workflows/docker-push.yml` **Trigger:** `push: tags: ['v*.*.*']` **Permissions:** `id-token: write` Each tagged build produces: - **SLSA provenance:** Includes the source URI and commit SHA. - **SBOM (SPDX 2.3):** Lists dependencies and package metadata. - **Immutable tags:** `docker.io/fystack/apex/{service}:{version}` and `{version}-{sha}`. **Verify Provenance** ```bash docker buildx imagetools inspect docker.io/fystack/apex/api:v1.0.0 \ --format '{{ json .Provenance.SLSA }}' | jq -r '.predicate.buildDefinition.resolvedDependencies[0].uri' # Expected: https://github.com/fystack/apex/commit/ ``` ### Cryptographic Signing (Cosign Keyless) Each image and attestation is signed by GitHub Actions' OIDC identity, eliminating private key management risks. Signatures are stored in **Rekor**, a public, append-only transparency log. **Signature Generation (within CI):** **Sign Image** ```bash cosign sign \ --identity-token "https://token.actions.githubusercontent.com" \ docker.io/fystack/apex/api@sha256: ``` **Signature Verification:** **Verify Signature** ```bash cosign verify \ --certificate-identity-regexp="github.com/fystack/apex/.github/workflows/docker-push.yml@*" \ --certificate-oidc-issuer="https://token.actions.githubusercontent.com" \ docker.io/fystack/apex/api:v1.0.0 ``` **This proves:** - The image was built by a trusted GitHub workflow. - The signature is valid and timestamped in Rekor. - No manual signing keys exist that could be stolen or misused. --- ## 3. Incident Verification & Recovery If a hack, image tampering, or insider attack is suspected, follow this verification sequence to prove authenticity: ### Step 1 — Retrieve Provenance **Retrieve Provenance** ```bash cosign verify-attestation --type slsaprovenance docker.io/fystack/apex/api:v1.0.0 | jq . ``` Reveals source commit URI, repository, and workflow identity. ### Step 2 — Verify Signature and Transparency Record **Verify Signature & Rekor Log** ```bash cosign verify \ --certificate-identity-regexp="github.com/fystack/apex/.github/workflows/docker-push.yml@*" \ --certificate-oidc-issuer="https://token.actions.githubusercontent.com" \ docker.io/fystack/apex/api:v1.0.0 rekor-cli search --artifact docker.io/fystack/apex/api:v1.0.0 ``` Confirms the image signature is valid and its record exists in Rekor's public log with a timestamp. ### Step 3 — Verify Git Commit Integrity **Verify Commit** ```bash git show ``` Confirms the commit referenced in provenance still exists and wasn't rewritten. ### Step 4 — Cross-Check SBOM **Verify SBOM** ```bash docker buildx imagetools inspect docker.io/fystack/apex/api:v1.0.0 | jq '.SBOM' ``` Confirms dependencies match the verified source repository and SBOM metadata. **Final Proof Chain:** **Proof Chain** ``` Rekor Timestamp → Cosign Signature → Provenance Commit URI → Git Commit Hash ``` If all four align, the image is proven to originate from a trusted, untampered commit. ### Verification Checklist | Step | Command | Expected Output | | --- | --- | --- | | 1. Verify Signature | `cosign verify ...` | Success message showing GitHub OIDC identity | | 2. Check Rekor Log | `rekor-cli search --artifact ` | Log entry with timestamp | | 3. Inspect Provenance | `cosign verify-attestation --type slsaprovenance ...` | Commit URI and workflow ID | | 4. Confirm Commit Exists | `git show ` | Commit metadata matches provenance | | 5. Validate SBOM | `docker buildx imagetools inspect ...` | SPDX 2.3 metadata present | --- ## 4. Hardening & Best Practices | Control | Purpose | | --- | --- | | Pin base images by digest | Prevents supply chain drift and tag rewriting. | | Immutable version tags | Disallow overwriting existing version tags. | | Require signed commits in GitHub | Ensures authenticated source commits. | | Registry content trust | Enforce OCI registry tag immutability. | | SBOM embedding | Ensures dependency traceability. | | Immutability policy enforcement | Prevent registry pushes to existing tags. | | Audit with immudb | Store verification results (image digest + commit SHA) in tamper-proof logs. | --- ## 5. Summary This build pipeline gives Fystack a **provable, cryptographically verifiable lineage** from Git commit to deployed container. It ensures: - **Transparent and auditable** supply chain provenance. - **No secret key dependency**. - **Resistance to insider or external tampering**. - **Compliance alignment** with SLSA Level 3 and Sigstore best practices. **In short:** every Fystack container is not just reproducible — it's **provably authentic**. --- # Deployment Best Practices Fystack enforces strict deployment security controls to protect production infrastructure and sensitive cryptographic material. This document outlines our core deployment security principles: network isolation, access control, and endpoint protection. --- ## 1. Private Network Deployment with VPN Access All Fystack production infrastructure is deployed within **private networks** with mandatory VPN access, ensuring that critical services are never directly exposed to the public internet. ### Network Architecture | Component | Network Configuration | | --- | --- | | Application Servers | Deployed in private subnets with no public IP addresses | | Database Servers | Isolated in dedicated database subnets, accessible only from application tier | | MPC Nodes | Deployed in highly restricted private network with peer-to-peer VPN mesh | | Admin Access | VPN required for all administrative operations and deployments | | Public Internet | Only load balancers and API gateways have public endpoints | ### VPN Access Requirements All personnel accessing production infrastructure must authenticate through VPN with the following controls: - **Multi-factor authentication (MFA):** Hardware security keys or authenticator apps required - **Device compliance:** Only managed devices with full disk encryption and endpoint protection - **IP allowlisting:** VPN access restricted to authorized IP ranges - **Session monitoring:** All VPN sessions logged and monitored for anomalies - **Automatic timeout:** VPN sessions expire after 8 hours of inactivity - **Certificate-based authentication:** Client certificates required in addition to credentials ### Network Segmentation | Network Tier | Access Control | | --- | --- | | Public Zone | Load balancers, CDN, DDoS protection only | | DMZ Zone | API gateway, reverse proxies, WAF | | Application Zone | Application servers, accessible from DMZ and VPN only | | Data Zone | Databases, accessible from application zone only | | Secure Zone | MPC nodes, KMS, secret stores - highest restriction level | --- ## 2. Cloud IAM Roles for KMS and Secret Manager Access Fystack uses **Cloud IAM roles** to enforce the principle of least privilege for access to Key Management Service (KMS) and Secret Manager. Only essential services and C-level executives have access to critical cryptographic material. ### IAM Access Model | Resource | Who Has Access | Permission Level | | --- | --- | --- | | KMS Master Keys | CEO, CTO only | Full access (encrypt, decrypt, manage) | | Secret Manager | CEO, CTO only | Full access (read, write, delete) | | Application Services | API service, MPC nodes (via service account) | Read-only access to specific secrets | | Developers | No access | No permissions to production KMS or secrets | | DevOps Team | No direct access | Can deploy, but cannot read secret values | ### Service Account Restrictions Application services access KMS and Secret Manager through **service accounts** with tightly scoped permissions: - **Service-specific accounts:** Each service has its own dedicated service account - **Minimum permissions:** Only permissions required for specific operations - **Secret-level access control:** Access granted per individual secret, not entire secret store - **Time-bound credentials:** Service account tokens expire and rotate automatically - **Audit logging:** All KMS and Secret Manager access logged to immutable audit trail ### Access Control Policies **KMS Access Policy Example:** - CEO and CTO can manage KMS keys and view audit logs - Application service accounts can only encrypt/decrypt with specific keys - All key usage is logged with caller identity and timestamp - Key deletion requires dual authorization from both CEO and CTO - Key rotation is automated and monitored **Secret Manager Access Policy Example:** - CEO and CTO can create, read, update, and delete secrets - Application services can only read secrets assigned to them - Secret access is logged with service identity and IP address - Secret versions are immutable and retained for audit purposes - Secret rotation triggers automatic service updates --- ## 3. Endpoint Detection and Response (EDR) Fystack deploys **Endpoint Detection and Response (EDR)** solutions such as **CrowdStrike** or **Bitdefender** as sidecars to detect and prevent infiltration attempts on all production systems. ### EDR Deployment Architecture | Component | EDR Protection | | --- | --- | | Container Sidecar | EDR agent deployed as sidecar container in every pod | | Process Monitoring | Real-time monitoring of all process executions and syscalls | | Network Traffic | Inspection of inbound and outbound network connections | | File Integrity | Detection of unauthorized file modifications | | Memory Scanning | Detection of memory-resident malware and code injection | ### EDR Capabilities **Threat Detection:** - **Behavioral analysis:** Identifies anomalous process behavior and privilege escalation - **Signature-based detection:** Blocks known malware and exploits - **Machine learning:** Detects zero-day threats and advanced persistent threats (APTs) - **Lateral movement detection:** Identifies unauthorized access attempts across systems - **Ransomware protection:** Detects and blocks file encryption attempts **Response Actions:** - **Automatic containment:** Isolate compromised containers from network - **Process termination:** Kill malicious processes automatically - **Forensic capture:** Preserve memory and disk state for investigation - **Alert escalation:** Immediate notification to security team - **Rollback capability:** Restore from known-good container images ### EDR Monitoring and Alerting | Alert Type | Response Action | | --- | --- | | Critical Threat Detected | Automatic container isolation, immediate security team notification | | Suspicious Process Execution | Process killed, alert sent to security operations center | | Unauthorized Network Connection | Connection blocked, network traffic logged for analysis | | File Integrity Violation | Container redeployed from verified image, incident logged | | Memory Injection Detected | Process terminated, memory dump captured, CEO/CTO notified | ### EDR Integration - **SIEM integration:** EDR events forwarded to Security Information and Event Management system - **Incident response playbooks:** Automated response workflows for common threat scenarios - **Threat intelligence feeds:** Real-time updates on emerging threats and indicators of compromise - **Compliance reporting:** Automated compliance reports for security audits - **Forensic analysis:** Detailed telemetry for post-incident investigation --- ## 4. Summary Fystack's deployment security strategy is built on three fundamental pillars: 1. **Private network isolation with VPN access** ensures critical infrastructure is never directly exposed to the internet 2. **Strict IAM controls** limit KMS and Secret Manager access to CEO and CTO only, with minimal service account permissions 3. **EDR sidecar deployment** provides real-time threat detection and automated response across all production systems These controls work together to create a defense-in-depth security posture that protects cryptographic keys, sensitive data, and production infrastructure from both external and internal threats. --- # Changelog New updates and improvements to Fystack. - [v0.3.1](https://docs.fystack.io/changelog/v0.3.1.md) (2026-10-03): Release notes for v0.3.1 including KYT screening with Webacy, detailed webhook delivery logs, workspace creation by owner invitation, and a refreshed dashboard. - [v0.3.0](https://docs.fystack.io/changelog/v0.3.0.md) (2026-09-15): Release notes for v0.3.0 including the multi-chain address book, sweep monitoring, plans and usage, workspace-wide MFA enforcement, second factor on every sign-in, and login alerts. - [v0.2](https://docs.fystack.io/changelog/v0.2.md) (2026-08-27): Release notes for v0.2 including Approval Groups for multi-signature governance of wallets and policies, Ed25519 Client Key Pair authentication for the API, and API key expiration dates. - [v0.1.16](https://docs.fystack.io/changelog/v0.1.16.md) (2026-07-31): Release notes for v0.1.16 including mandatory email verification, a redesigned Withdrawal Center, live multi-signature approval tracking, wallet balance and overview endpoints, and analytics permission scoping. - [v0.1.15](https://docs.fystack.io/changelog/v0.1.15.md) (2026-06-30): Release notes for v0.1.15 including Programmable Policy Engine v2, cryptographically signed policy bundles, custom role management, granular per-module permissions, and live policy reload. - [v0.1.14](https://docs.fystack.io/changelog/v0.1.14.md) (2026-05-31): Release notes for v0.1.14 including the self-hosted license key system, real-time notifications over Server-Sent Events, API key IP whitelisting, the first wave of role-based access control, and an RFC 8785 canonical JSON signing fix. - [v0.1.13](https://docs.fystack.io/changelog/v0.1.13.md) (2026-04-28): Release notes for v0.1.13 including Gas Sponsorship (Auto-Fuel) for EVM, Solana, and TRON, a redesigned multi-withdrawal tracker, per-field permissions for wallet settings, network-scoped wallets, and wallet lifecycle actions. - [v0.1.12](https://docs.fystack.io/changelog/v0.1.12.md) (2026-03-31): Release notes for v0.1.12 including passkey-based 2FA, workspace rename and delete, human-readable asset identifiers, and cross-tab session coordination. - [v0.1.11](https://docs.fystack.io/changelog/v0.1.11.md) (2026-02-28): Release notes for v0.1.11 including KYT integration, Tron savings, role-based alert policies, and a cancel transaction API. - [v0.1.10](https://docs.fystack.io/changelog/v0.1.10.md) (2026-01-31): Release notes for v0.1.10 including Bitcoin support, idempotency keys, withdrawal approvals UI, and the new Alerts module. - [v0.1.9](https://docs.fystack.io/changelog/v0.1.9.md) (2025-12-14): Release notes for v0.1.9 including proposer role, withdrawal categories, expense dashboard, balance query improvements, and MPCIUM key derivation enhancements. - [v0.1.8](https://docs.fystack.io/changelog/v0.1.8.md) (2025-11-11): Release notes for v0.1.8 including security enhancements, Tron improvements, alert features, and infrastructure upgrades. - [v0.1.7](https://docs.fystack.io/changelog/v0.1.7.md) (2025-09-16): Release notes for v0.1.7 including TRON blockchain support, high availability features, and major infrastructure improvements. - [v0.1.6](https://docs.fystack.io/changelog/v0.1.6.md) (2025-08-21): Release notes for v0.1.6 including withdrawal improvements, webhook enhancements, and API additions. - [v0.1.5](https://docs.fystack.io/changelog/v0.1.5.md) (2025-08-12): Release notes for v0.1.5 including major enhancements, new APIs, improved reliability, and extensive bug fixes. --- # v0.3.1 ## Release v0.3.1: 2026-10-03 This release adds **Webacy** as a KYT provider, **detailed webhook delivery logs**, and a **refreshed dashboard**. **Included updates:** - Apex Platform (v0.3.1) - Fystack UI (v0.3.1) ## New Features ### KYT Screening with Webacy Webacy is now available as a KYT provider for screening incoming deposits. **Highlights:** - Select Webacy as the KYT provider in workspace settings - Deposits are screened against the sender's risk and sanctions status - Screening results appear in the dashboard and in the `deposit.confirmed` webhook - New **Review KYT** permission controls who can review screening results --- ### Webhook Delivery Logs Webhook logs now show the full request and response for each delivery attempt, so failed deliveries are easier to debug. See [Webhooks](https://docs.fystack.io/webhooks.md) for event payloads. --- ### Workspace Creation by Owner Invitation System admins can create a new workspace by inviting its owner from the admin console. The owner sets up their account and workspace from the invitation email. --- ### Refreshed Dashboard **Highlights:** - Redesigned sidebar - Notification center with read and unread tabs - Onboarding questionnaire for new accounts - Support menu for reaching the Fystack team --- ## Summary Apex v0.3.1 brings **KYT screening with Webacy**, **detailed webhook logs**, and a **refreshed dashboard**: - **Webacy KYT** screens deposits and reports results in the dashboard and webhooks - **Webhook logs** show the full request and response for each delivery - **Refreshed dashboard** adds a redesigned sidebar and a notification center --- --- # v0.3.0 ## Release v0.3.0: 2026-09-15 This release adds a **multi-chain address book**, **sweep monitoring**, and **plans and usage** for workspaces. On the security side, owners can now **require MFA across their workspace**, a second factor is checked on **every sign-in**, and users get **email alerts** for new and failed logins. **Included updates:** - Apex Platform (v0.3.0) - Fystack UI (v0.3.0) ## New Features ### Multi-Chain Address Book A single address book entry can now hold one address per chain, so a counterparty is saved once instead of once per network. **Highlights:** - Add addresses for several chains under one named entry - Sweeps and withdrawals to external addresses can target any address book entry, including multi-chain wallets - Admins can update and delete address book records --- ### Sweep Monitoring Sweeps are now visible and recoverable from the dashboard. **Highlights:** - Sweep attempt history per target, with status, failures, transaction links, and estimated next run - Repeatedly failing sweeps pause automatically instead of retrying forever - **Reset** a paused or stuck sweep to try again - Solana sweeps can pay their own network fees before drawing from the Gas Tank See [Automation](https://docs.fystack.io/product/automation.md) for sweep setup. --- ### Plans and Usage Workspaces now run on a plan with usage limits. **Highlights:** - New **Plan & Usage** page showing the current plan and how much of each limit is used - Plan limits such as wallet count are enforced when creating resources - Self-hosted licenses unlock enterprise features per license --- ### More Updates - **Admin console**: system admins can view users, transactions, and audit trails across workspaces - **Transactions**: search, plus CSV export in local time or UTC - **Networks**: rescan a network from the dashboard - **API keys**: key rotation and unique key names --- ## Security ### Workspace MFA Enforcement Owners can require MFA for every member of a workspace. Members without MFA are asked to enroll before they can continue. ### 2FA for Sensitive Actions Withdrawing, approving, and managing roles now require 2FA. Users without 2FA are prompted to enroll first. ### Second Factor on Every Sign-In When MFA is enabled, a passkey or TOTP code is required after the password or Google step, on every sign-in. ### Login Alerts Users get an email for each new sign-in to their account and for repeated failed login attempts. ### Invitation Acceptance Workspace invitations must be explicitly accepted, and the invite shows who sent it. See [Security](https://docs.fystack.io/product/security.md) for account security settings. --- ## Breaking Changes ### Wallet Auto-Approval Limit Removed The wallet-level **auto-approval limit** setting has been removed. Auto-approval is now configured only through [policies](https://docs.fystack.io/product/policies.md). **Before upgrading:** recreate any wallet-level auto-approval limit as a policy approval-bypass rule. --- ## Summary Apex v0.3.0 is the **operations and account security release**: - **Multi-chain address book** saves one counterparty across every chain - **Sweep monitoring** shows every sweep attempt and lets you reset stuck sweeps - **Plans and usage** make workspace limits visible - **Workspace MFA enforcement**, **2FA for sensitive actions**, and a **second factor on every sign-in** tighten account access - **Login alerts** notify users of new and failed sign-ins --- --- # v0.2 ## Release v0.2: 2026-08-27 This release introduces **Approval Groups**, a named set of members and a signature threshold that governs withdrawals, policies, and group membership itself. It also adds two API security features: **Client Key Pair authentication** using Ed25519, and **expiration dates on API keys**. **Included updates:** - Apex Platform (v0.2) - Fystack UI (v0.2) ## New Features ### Approval Groups An approval group is a named set of workspace members plus the number of signatures required from them before a sensitive action takes effect. Assigning one to a wallet means no withdrawal leaves that wallet until enough members sign off. _Image: Approval Group tab under User Management showing the Default Approval Group with its threshold, status, and members_ **Highlights:** - New **Approval Group** tab under User Management, with creation restricted to the workspace Owner - Set a signature threshold per group, bounded by the number of members selected - Assign a group to a **wallet** so only its members can approve withdrawals from that wallet - Assign a group to a **policy** so policy and rule changes need sign-off before they apply - Creating a group, changing its membership, and changing a wallet's assigned group are each approval-gated, so no single person can grant themselves signing authority - New **Approval Group Request** tab under Approval for reviewing pending requests, with per-reviewer status and an optional comment on approve or reject - Every workspace starts with a **Default Approval Group** that reviews the creation of other groups See [Approval Groups](https://docs.fystack.io/product/approval-group.md) for the full setup guide. --- ### Client Key Pair Authentication The API now supports **Ed25519 client key pairs** for request signing, so the credential used to sign requests never has to be stored by Fystack. _Image: Create API Key dialog with Client Key Pair selected as the recommended authentication method, using an Ed25519 public key with no secret stored_ **Highlights:** - Register an Ed25519 public key when creating an API key, and keep the private key on your side - Generate the key pair locally, or keep it in **AWS KMS** so the private key is non-exportable - The Create API Key flow now asks how the key should authenticate before issuing credentials See [API Keys](https://docs.fystack.io/api-keys.md#client-key-pair) for key generation and SDK usage. --- ### API Key Expiration API keys can now carry an expiration date, which makes rotation routine instead of manual. **Highlights:** - Set an expiration date when creating an API key - Reduces the risk of long-lived credentials sitting unused --- ## Recommended for Production Integrations - Enable **IP whitelisting** to restrict where an API key can be used from, available since v0.1.14 - Set an **expiration date** on API keys and rotate them regularly - Protect private keys with hardware-backed key management such as **AWS KMS** - Give production wallets an approval group with a threshold of at least 2 --- ## Summary Apex v0.2 is the **governance and API security release**: - **Approval Groups** bring multi-signature control to withdrawals, policies, and group membership, with creation and membership changes gated behind an existing group - **Client Key Pair authentication** lets an Ed25519 private key stay in your own infrastructure or in AWS KMS - **API key expiration** makes credential rotation a routine step rather than a manual cleanup --- --- # v0.1.16 ## Release v0.1.16: 2026-07-31 This release pairs a **sign-up and sign-in hardening pass**, including mandatory email verification, with a rebuilt **Withdrawal Center** that tracks multi-signature approvals live. **Included updates:** - Apex Platform (v0.1.16) - Fystack UI (v0.1.16) ## New Features ### Mandatory Email Verification Sign-in now requires a verified email address. **Highlights:** - A verified email address is required to sign in - The verification flow was hardened mid-rollout to close an **email-enumeration gap**, where sign-in behavior could reveal whether an address was registered --- ### Redesigned Withdrawal Center A rebuilt withdrawal list, detail view, and step tracker replaces the legacy withdrawal tracker, built for higher transaction volumes. **Highlights:** - New withdrawal list, detail view, and step tracker - Live status polling - Clearer per-step progress --- ### Multi-Signature Approval Tracking Withdrawal details now show approval status for each signer as it happens. **Highlights:** - Live approval status per signer on the withdrawal detail view - Policy rules can automatically bypass manual approval for withdrawals that qualify See [Approval Groups](https://docs.fystack.io/product/approval-group.md) for how signers and thresholds are configured. --- ### Wallet Balances and Overview API New endpoints expose aggregated wallet data for integrators. **Highlights:** - New endpoints for aggregated **wallet balance** and **overview** data - Withdrawal responses now include wallet details and approval counts --- ### Analytics Permission Scoping Analytics routes now sit behind a dedicated RBAC permission instead of being reachable by any authenticated role. --- ## Summary Apex v0.1.16 is the **authentication and withdrawal tracking release**: - **Mandatory email verification** on sign-in, with an email-enumeration gap closed during rollout - **Redesigned Withdrawal Center** replaces the legacy tracker with live polling and per-step progress - **Multi-signature approval tracking** shows each signer's status live, with policy-driven approval bypass - **Wallet balance and overview endpoints** plus approval counts on withdrawal responses - **Analytics permission scoping** puts analytics behind a dedicated RBAC permission --- --- # v0.1.15 ## Release v0.1.15: 2026-06-30 This release is a ground-up rebuild of policy and access control. It introduces **Programmable Policy Engine v2** with signed, tamper-evident bundles, and a complete **RBAC overhaul** with granular permissions and self-service role management. **Included updates:** - Apex Platform (v0.1.15) - Fystack UI (v0.1.15) ## New Features ### Programmable Policy Engine v2 A new policy engine evaluates withdrawal and contract-call actions against configurable rules, replacing the previous engine. **Highlights:** - Evaluates **withdrawal** and **contract-call** actions against configurable rules - Conflict detection across rules - Full rule-builder UI for defining conditions, wallet targeting, and approval-group bindings See [Policies](https://docs.fystack.io/product/policies.md) for the rule fields and condition syntax. --- ### Signed Policy Bundles Policy bundles and cache entries are now cryptographically signed and verified, so policy that is served or cached can be checked for tampering. **Highlights:** - Policy bundles are cryptographically **signed and verified** - Cache entries are covered by the same verification - Served or cached policy can be checked for tampering --- ### Custom Role Management Admins can now define their own roles instead of choosing from a fixed list. **Highlights:** - Create, edit, and delete custom roles from a new role management UI - Permission matrix prevents granting access above your own level --- ### Granular Permissions Workspace-level permissions were broken up so access can be granted per module rather than all at once. **Highlights:** - Coarse workspace-level permissions split into **per-module read/write permissions** - Legacy implicit wallet-role system removed in favor of explicit wallet assignments - The `guest` role was retired in favor of `viewer` --- ### Live Policy Reload RBAC and policy changes now sync and reload without a service restart. --- ## Summary Apex v0.1.15 is the **policy and access control release**: - **Programmable Policy Engine v2** evaluates withdrawals and contract calls against configurable rules, with conflict detection and a rule-builder UI - **Signed policy bundles** make served and cached policy tamper-evident - **Custom role management** lets admins define roles themselves, bounded by their own access level - **Granular permissions** replace coarse workspace-level access with per-module read/write control - **Live policy reload** applies RBAC and policy changes without a restart --- --- # v0.1.14 ## Release v0.1.14: 2026-05-31 This release adds **license key enforcement** for self-hosted deployments, **real-time notifications** delivered over Server-Sent Events, **API key IP whitelisting**, and the first wave of **role-based access control** ahead of the full RBAC rollout in v0.1.15. **Included updates:** - Apex Platform (v0.1.14) - Fystack UI (v0.1.14) ## New Features ### License Key System Self-hosted deployments are now gated by a signed license key, so an installation can be verified as licensed before it runs. **Highlights:** - Deployments are gated by a **signed license key**, verified against a public key - New keygen CLI for issuing keys --- ### Real-Time Notifications Notifications now stream to clients over **Server-Sent Events** instead of being polled, so alerts and status updates arrive instantly. **Highlights:** - Delivery moved from polling to **Server-Sent Events** - Instant delivery of alerts and status updates --- ### API Key IP Whitelisting API keys can now be restricted to a whitelist of client IP addresses, which limits what an attacker can do with a leaked key. **Highlights:** - Restrict an API key to a whitelist of client IP addresses - Whitelists can be updated after the key is created - Reduces the blast radius of a leaked key --- ### RBAC Foundations The first stage of role-based access control, laying the groundwork for the full overhaul in v0.1.15. **Highlights:** - Roles are assigned on workspace invitation - Roles are surfaced in workspace API responses - New audit trail records role changes --- ## Fixes ### Canonical JSON Signing Fixed JSON canonicalization to follow **RFC 8785**, addressing a pentest finding. This affects anything hashed or signed against a canonical payload. --- ## Summary Apex v0.1.14 is the **licensing and access foundations release**: - **License key system** gates self-hosted deployments behind a signed, verifiable key - **Real-time notifications** replace polling with Server-Sent Events - **API key IP whitelisting** limits the damage a leaked key can do - **RBAC foundations** introduce roles on invitation, in API responses, and in a new audit trail - **RFC 8785 canonicalization** closes a pentest finding affecting signed payloads --- --- # v0.1.13 ## Release v0.1.13: 2026-04-28 This release introduces **Gas Sponsorship (Auto-Fuel)** for EVM, Solana, and TRON, an end-to-end **Auto Gas Refill** experience in the dashboard, a redesigned **Withdrawal Tracker** that handles multiple in-flight withdrawals, and per-field permissions for wallet settings. **Included updates:** - Apex Platform (v0.1.13) - Fystack UI (v0.1.13) ## New Features ### Gas Sponsorship (Auto-Fuel) Workspaces can now opt wallets into **gas sponsorship**, where the platform automatically funds the gas needed to move tokens so end users no longer need to hold the native chain coin. **Highlights:** - Coverage across **EVM, Solana, and TRON** — sponsor gas for ERC-20s, SPL tokens, and TRC-20s out of a single workspace gas tank - New `gas_sponsorship_enabled` flag on wallet settings, set at creation or from the wallet settings page - New `FUNDING_GAS` withdrawal status and `withdrawal.funding_gas` webhook so the sponsorship step is visible in the lifecycle and integrations - Withdrawal preview now shows the expected top-up amount up front - Withdrawal flow automatically waits for funding before broadcasting **Use cases:** - Onboard end users without forcing them to acquire ETH, MATIC, SOL, or TRX just to move their tokens - Centralize gas funding into a workspace-managed gas tank instead of topping up wallets manually - Build consumer-facing flows where the app sponsors fees transparently --- ### Auto Gas Refill UI The Auto-Fuel feature has a complete operator experience in the dashboard. **Highlights:** - New **Auto Gas Refill** modal with validation and per-network configuration - Wallet settings page exposes auto gas refill configuration alongside network selection - Gas sponsorship messaging surfaced in the create-wallet and send/withdraw forms so users know when gas is sponsored - Transaction history now labels the gas-funding step so it's clear in past activity --- ### Withdrawal Tracker The withdrawal tracker has been redesigned to keep up with multiple concurrent withdrawals and the new gas-funding step. **Highlights:** - Supports multiple concurrent in-flight withdrawals on a single screen - Each withdrawal shows its own progress states, including the new **Funding Gas** step - Refreshed layout and copy across the withdraw flow --- ### Wallet Settings: Per-Field Permissions Wallet settings are now governed by **per-field permissions** instead of a single all-or-nothing control, so admins can manage day-to-day preferences without owner-level access. **Highlights:** - Admins can update non-sensitive fields such as the wallet name and gas sponsorship toggle - Sensitive fields (signing threshold, disabled state, auto-approval limit) still require the wallet owner - Clear error message lists exactly which fields are blocked when a permission is missing This closes a long-standing gap where every settings change required the wallet owner. --- ### Network-Scoped Wallets Wallet creation and listing are now **network-aware**, so workspaces operating on a curated set of chains can scope wallets without leaking unrelated networks into the UI. **Highlights:** - New network selector in the create-wallet form - Wallet list and filters honor the selected networks - Cleaner network metadata across forms and modals --- ### Wallet Lifecycle: Delete, Disable, and Remove Asset Wallet owners now have a full lifecycle UI for cleaning up wallets, with the appropriate guardrails. **Highlights:** - Delete a wallet directly from the wallet table (owner-only) - Disable a wallet to pause activity without deleting it - Remove an individual asset from a wallet - New wallet purpose dropdown in the create-wallet form --- ### Notification and Modal Polish The in-app notification surface and a few high-traffic modals received a cleanup pass. **Highlights:** - Refreshed notification icons and grouping for deposits, withdrawals, and other events - Deposit and withdraw modals show the correct network logo dynamically - Asset views fall back to the asset name when no avatar is available - Tighter styling for pending-withdrawal alerts and transaction items --- ## Summary Apex v0.1.13 is the **gas sponsorship release**: - **Auto-Fuel** lets workspaces sponsor gas for **EVM, Solana, and TRON** token movements end-to-end, with a full **Auto Gas Refill** experience in the dashboard - **Withdrawal Tracker** is rebuilt for multiple concurrent withdrawals and surfaces the gas-funding step - **Per-field permissions** on wallet settings finally separate day-to-day preferences from sensitive controls - **Network-scoped wallets** and **wallet delete / disable / remove-asset** round out the operational surface --- --- # v0.1.12 ## Release v0.1.12: 2026-03-31 This release introduces passkey-based two-factor authentication, workspace rename and delete operations, human-readable asset identifiers in client-facing APIs, and improved cross-tab session handling. **Included updates:** - Apex Platform (v0.1.12) - Fystack UI (v0.1.12) ## New Features ### Passkey Two-Factor Authentication The platform now supports **passkey-based 2FA** as an alternative to time-based one-time passwords, bringing phishing-resistant authentication to all workspaces. **Highlights:** - Register one or more passkeys per user from the security settings - Use platform authenticators (Touch ID, Face ID, Windows Hello) or external security keys - Step-up authentication for sensitive operations using a registered passkey - Coexists with the existing TOTP-based 2FA so users can migrate at their own pace **Benefits:** - Strong phishing resistance compared to TOTP - Faster sign-in and approval flows - Better experience on mobile devices with biometric authenticators --- ### Workspace Rename and Delete Workspace administrators can now rename or delete workspaces directly from the UI. **Highlights:** - Rename a workspace from the workspace settings page - Permanent workspace deletion with the appropriate guardrails - Updated workspace selector and management UI to reflect changes immediately - Supporting API endpoints for both operations This closes a long-standing gap for teams that needed to clean up legacy workspaces or rebrand existing ones. --- ### Human-Readable Asset Identifiers Client-facing APIs for withdrawals and checkout now accept **human-readable asset identifiers** in place of internal IDs, making integrations more intuitive. **Highlights:** - The `symbol` field has been renamed to `asset` in client-facing API fields - Asset identifiers like `USDC` or `USDC.eth` can be used directly in API calls - New asset resolver maps human-readable identifiers to the correct network and contract - Added unit tests covering asset resolution edge cases **Use cases:** - Easier API onboarding for new integrators - Reduced need to look up internal asset IDs when scripting flows - More readable request and response payloads --- ### Auto-Accept Workspace Invitations Pending workspace invitations are now automatically accepted when the invited user signs in, removing a redundant click from the onboarding flow. **Highlights:** - Invitations linked to the signing-in user are accepted on first login - Workspace list refreshes immediately after acceptance - Improved query invalidation so newly joined workspaces appear without manual reload --- ### Cross-Tab Session Coordination The UI now coordinates sessions across browser tabs to handle the case where the active workspace changes in one tab. **Highlights:** - New session coordination hook detects workspace mismatches across tabs - Graceful handling when a user switches workspaces in another tab - Tab-aware session refresh to prevent stale UI state - Reduced risk of acting on the wrong workspace in multi-tab workflows --- ## Summary Apex v0.1.12 strengthens identity, workspace lifecycle, and integration ergonomics: - **Passkey 2FA** brings phishing-resistant authentication to all users - **Workspace rename and delete** finally let admins manage workspace lifecycle - **Human-readable asset identifiers** make the public API friendlier to integrators - **Auto-accept invitations** and **cross-tab session coordination** smooth out everyday flows --- --- # v0.1.11 ## Release v0.1.11: 2026-02-28 This release introduces transaction risk screening through a KYT integration, Tron savings to reduce on-chain costs, role-aware alert and webhook policies, and a new cancel transaction API. **Included updates:** - Apex Platform (v0.1.11) - Fystack UI (v0.1.11) ## New Features ### KYT Integration with Scorechain The platform now supports Know-Your-Transaction (KYT) screening through a Scorechain provider integration, enabling automated risk assessment on incoming and outgoing transfers. **Highlights:** - New KYT provider abstraction with Scorechain as the first supported backend - Risk scoring applied to deposits and withdrawals - KYT configuration surfaced in the workspace settings UI - Risk results integrated into transaction views and approval flows **Use cases:** - Block or flag high-risk counterparties before funds are released - Maintain a compliance audit trail for screened transactions - Centralized configuration of screening thresholds per workspace --- ### Tron Savings A new Tron savings feature optimizes how the platform interacts with the TRON network to reduce energy and bandwidth costs for sweeps and withdrawals. **Highlights:** - Smarter resource usage when broadcasting TRX and TRC20 transactions - Reduced operational cost on TRON without sacrificing reliability - Tighter integration with the existing sweeper and withdrawal flows --- ### Role-Based Alert and Webhook Policies Alert and webhook policies have been extended to be aware of the new role model, including the **Signer** and **Proposer** roles introduced in earlier releases. **Highlights:** - Alerts can now target specific roles instead of individual users - Webhook delivery rules respect role-based scoping - Cleaner separation between proposer-initiated and signer-approved events This makes it easier for operations teams to wire alerts to the right people without rebuilding routing every time membership changes. --- ### Cancel Transaction API A new endpoint allows authorized users to cancel a transaction that has not yet been confirmed on-chain. **Highlights:** - New `cancel transaction` API exposed alongside the existing transaction endpoints - Holds placed during transaction creation are automatically released on cancel - Front-end approval and dApp transaction flows updated to handle the cancel state cleanly --- ### Solana Rent Recovery and Gas Station The Solana flow now closes empty token accounts after sweeps to recover SOL rent, which is then routed back into the gas station for future transaction fees. **Highlights:** - Automatic close of empty SPL token accounts after a successful sweep - Recovered rent funds the gas station, reducing top-up frequency - New one-time-use account type for short-lived Solana operations --- ### Webhook CIDR Allowlist Webhook delivery now supports a CIDR-based allowlist so workspaces can restrict outbound webhook destinations to known networks. **Highlights:** - CIDR ranges configurable per workspace - Validation enforced at the time the webhook is created or updated - Reduces the blast radius if a webhook URL is ever misconfigured --- ## Summary Apex v0.1.11 focuses on compliance, cost, and control: - **KYT screening** brings automated risk assessment into deposits and withdrawals - **Tron savings** lower the operational cost of running the platform on TRON - **Role-aware policies** make alerts and webhooks easier to target and maintain - **Cancel transaction API** gives operators a clean way to abandon stuck transactions - **Solana rent recovery** and the **webhook CIDR allowlist** tighten cost and security --- --- # v0.1.10 ## Release v0.1.10: 2026-01-31 This release introduces native Bitcoin support across the platform, idempotency key handling for safer API operations, a redesigned Withdrawal Approvals experience, and a refactored Alerts module replacing the legacy Notifications surface. **Included updates:** - Apex Platform (v0.1.10) - Fystack UI (v0.1.10) ## New Features ### Native Bitcoin Support Bitcoin is now a first-class asset on the platform, with end-to-end support for deposits, sweeps, and withdrawals on both mainnet and testnet. **Platform capabilities:** - Bitcoin RPC container and dedicated handler integrated into the sweeper service - UTXO model, repository, and service with reorg handling and immature-status tracking - Multi-input Bitcoin signing flow built on top of the MPC signer - HD wallet key generation for Bitcoin addresses - RBF (Replace-By-Fee) tracking for in-flight transactions - Bitcoin testnet and mainnet network configurations out of the box **UI capabilities:** - Bitcoin address support and validation in send and address book flows - Updated wallet selector and asset views to surface BTC balances and networks This brings Bitcoin into the same operational model as the EVM and Solana stacks, with sweeping, balance reconciliation, and withdrawal flows all driven by the unified backend. --- ### Idempotency Keys for Critical APIs A new idempotency middleware allows clients to safely retry critical write operations without risking duplicate side effects. **Highlights:** - Idempotency key middleware with a Redis-backed cache - Configurable TTL for stored idempotency keys via `config.yaml` - Applied to high-risk endpoints such as withdrawal creation **Benefits:** - Safe retries for clients on flaky networks - Eliminates duplicate withdrawals caused by client-side timeouts - Predictable behavior for integrators building automated workflows --- ### Withdrawal Approvals Experience The withdrawal approvals flow received a major overhaul on both the API and the UI. **API changes:** - `WithdrawalApprovalResponse` now includes `UpdatedAt` and uses a value type for the `User` field - Transaction details endpoint returns associated withdrawal info - New `CreateWithdrawalResponse` type returned from `CreateWithdrawal` - Proposers can now cancel their own withdrawals **UI changes:** - New Withdrawal Approvals view with notes and richer status handling - Refactored Approval components for clearer review actions and consistent styling - Improved approval modal driven by the new `ApprovalModalUI` --- ### Alerts Module The legacy Notifications surface has been refactored into a dedicated **Alerts** module with its own routes and navigation entry. **Highlights:** - New alerts module wired into the main routes configuration - Cleaner separation between in-app notifications and operational alerts - Updated UI components for a consistent alert experience across pages --- ### Workspace and Account Improvements - Workspace name can now be updated directly from settings - Business email is used when sending operational notifications - Broadcasting transaction status added to expose in-flight broadcast state - Two-factor authentication modal auto-triggers verification on open - Token Refresh Manager introduced in the UI for more reliable session handling --- ## Summary Apex v0.1.10 expands the platform's asset coverage and hardens the most critical write paths: - **Bitcoin** is now natively supported across signing, sweeping, and balance flows - **Idempotency keys** make withdrawal and other write APIs safe to retry - **Withdrawal approvals** got a refreshed API surface and a redesigned UI - **Alerts** replace the old Notifications module with a dedicated experience - **Workspace settings** and session handling received quality-of-life improvements --- --- # v0.1.9 ## Release v0.1.9 — 2025-12-14 This release introduces enhanced role-based access control with the new Proposer role, improved withdrawal management with categories, comprehensive expense analytics, and advanced querying capabilities for workspace balances. **Included updates:** - Apex Platform (v0.1.9) - [Mpcium](https://github.com/fystack/mpcium) (v0.3.4) ## New Features ### Proposer Role A new **Proposer** role has been added to the permission system. Users with the Proposer role can: - Submit withdrawal proposals and transaction requests - View pending approvals and transaction history - **Cannot** approve or cancel withdrawal requests _Image: image-2025-12-14T03-38-30-444Z_ This role is ideal for operational teams who need to initiate transactions while maintaining separation of duties for approval workflows. **Key benefits:** - Enhanced security through role separation - Better compliance with internal controls - Reduced risk of unauthorized approvals - Clear audit trail for transaction proposals --- ### Withdrawal Categories Withdrawals now support custom categories, allowing better organization and tracking of fund movements. **Features:** - Assign categories to withdrawals (e.g., "Operational", "Payroll", "Refunds") - Filter and search withdrawals by category - Include category information in CSV exports - Track expenses by category in the analytics dashboard _Image: image-2025-12-14T03-39-13-435Z_ This feature enables better financial reporting and helps teams understand cash flow patterns across different business functions. --- ### Expense Dashboard _Image: image-2025-12-14T03-39-47-251Z_ A comprehensive **Expense Analytics Dashboard** provides real-time insights into workspace spending: **Overview Metrics:** - Total volume with percentage change indicators - Total transaction count with trend analysis - Average transaction size calculation **Expense by Category:** - Visual breakdown of expenses across categories - Donut chart showing category distribution - Percentage and dollar amount for each category **Monthly Breakdown:** - Bar chart visualization of monthly expense trends - Comparison between operational and uncategorized expenses - Historical view of spending patterns **Top Transactions:** - List of largest expense transactions - Detailed information including asset, wallet, amount, USD value, description, category, and timestamp - Quick identification of significant expenses --- ### Get Balances by Symbol New API endpoint to query workspace balances filtered by asset symbol. [View full documentation →](https://docs.fystack.io/workspaces.md#get-workspace-balances-by-symbol) **Endpoint:** `GET /workspaces/{workspace_id}/balances?symbol=USDC` **Features:** - Query balances for a specific token across all networks - Consolidated view of asset distribution - Network-specific balance breakdown - Wallet count per network - Asset metadata including logos and network information **Use cases:** - Quickly view total USDC holdings across all chains - Monitor specific token distributions - Aggregate balance reporting for accounting - Multi-chain asset reconciliation --- ### Transaction CSV Export Enhancement The transaction CSV export feature now includes the **withdrawal_id** field, enabling: - Easy correlation between transactions and withdrawal requests - Improved reconciliation workflows - Better audit trail for approved withdrawals - Enhanced financial reporting capabilities **CSV columns now include:** - Transaction ID - **Withdrawal ID** (new) - Asset details - Amount and USD value - Wallet information - Category - Timestamp - Network information --- ## MPCIUM v0.3.4 **GitHub:** [fystack/mpcium](https://github.com/fystack/mpcium/releases/tag/v0.3.4) ### Child Key Derivation Support MPCIUM now supports **child key derivation** for both ECDSA and EdDSA signature schemes, enabling hierarchical deterministic (HD) wallet functionality in distributed MPC environments. **Key enhancements:** - **ECDSA child key derivation** — Full support for BIP32-style key derivation for Bitcoin, Ethereum, and EVM-compatible chains - **EdDSA child key derivation** — Extended key derivation support for Solana, Cardano, and other EdDSA-based networks - **HD wallet compatibility** — Derive multiple keys from a single master key across distributed MPC nodes - **Deterministic key generation** — Recreate child keys from derivation paths without exposing parent keys **Benefits:** - Simplified key management for multi-chain wallets - Enhanced privacy through address rotation - Secure key derivation without centralizing trust **Use cases:** - Generating unique deposit addresses for users - Multi-account wallet structures --- ## Summary Apex v0.1.9 delivers significant improvements in access control, financial operations, and analytics: - **Enhanced governance** with the new Proposer role for better separation of duties - **Improved expense tracking** through withdrawal categories and analytics dashboard - **Better reporting** with categorized expenses and withdrawal IDs in CSV exports - **Advanced querying** with symbol-based balance lookups across networks - **MPCIUM enhancements** enabling HD wallet support for ECDSA and EdDSA These features strengthen Fystack's position as a comprehensive digital asset custody and management platform, with improved controls, visibility, and operational efficiency. --- --- # v0.1.8 ## Release v0.1.8 — 2025-11-11 This release focuses on strengthening security and data integrity across wallet operations, enhancing Tron transaction reliability and sweep logic, improving observability through alerts, webhooks. **Included updates:** - Apex Platform (v0.1.8) - [Mpcium](https://github.com/fystack/mpcium) (v0.3.3) - [Multichain Indexer](https://github.com/fystack/multichain-indexer) (v1.0.10) ## Responsive Improvements _Image: image-2025-11-11T14-58-30-582Z_ ## Display withdrawal detail in withdrawal approvals _Image: image-2025-11-11T15-04-12-416Z_ ## Support sorting wallets by balances _Image: Sort wallets by balance_ ## Core Wallet & Sweep Engine - Improved reliability and consistency across multi-chain sweeping and finalization logic - Enhanced transaction validation and confirmation handling for supported networks - Simplified internal coin-mapping for better network interoperability - Improved fee estimation and transfer reliability - Optimized performance for Tron-related operations --- ## Security & Compliance - Implement CSRF protection - Multiple security hardening improvements across authentication, encryption, and session management - Enhanced protection against unauthorized access and brute-force attempts - Strengthened data integrity and compliance mechanisms --- ## Infrastructure & DevOps - Migrated balance store from Consul to database for higher reliability - Updated Docker image workflows with SLSA provenance and attestation - Initialized attestation system for verifiable build integrity - Added heartbeat monitor for network health checks - Allow diable websocket transaction listener --- ## Monitoring & Alerts - Implemented alert feature with Telegram integration - Added API to delete or disable alerts - Added webhook logs, resend option, and webhook statistics API for observability --- ## Transaction processor improvements - Added support for SLIP-10 key derivation - Adjusted key derivation logic for multi-chain compatibility - Added fallback USDT price provider to improve price reliability - Prevented dust attacks on small-value transfers - Refactored withdrawal validation and fee estimation logic - Excluded Solana rent fee from max native withdrawal calculations --- ## Quality Improvements - Enhanced API validation and parameter safety - Added integrity checks for webhook creation and event insertion - Improved transaction listener logs and resiliency - Updated API responses for pending blocks to reflect accurate status --- ## Miscellaneous - Moved Telegram bot to high-availability service - Implemented service rescanning endpoint - Improved SDK API structure for better developer experience - Added CSV export functionality for transactions --- ## Summary Apex v0.1.8 marks a stable milestone toward production-grade custody infrastructure with: - Strengthened **security and data integrity** across wallet operations - Enhanced **Tron transaction reliability and sweep logic** - Improved **observability** through alerts, webhooks, and health monitoring - Upgraded **DevOps and build pipelines** with verifiable provenance (SLSA) --- ## Multichain Indexer v1.0.10 **GitHub:** [fystack/multichain-indexer](https://github.com/fystack/multichain-indexer) ### Enhancements - **Improved Compatibility** — Enhanced cross-chain compatibility and standardized behavior across providers _(by @anhthii #15)_ - **EVM Indexer Performance Boost** — Optimized batch processing and block scanning to improve indexing throughput _(by @anhthii #23)_ - **Chunking and Load Balancing Improvements** — Refactored `chunkBySize` and round-robin logic for more efficient load distribution and fallback to best provider during catch-up _(by @vietddude #22)_ - **Health Check & Docker Deployment** — Added health endpoint, improved Docker deployment workflow, and standardized package paths _(by @anhthii #18)_ - **Transfer Event Handling** — Added clear instructions for consuming transfer events and performed related code cleanup _(by @anhthii #17)_ ### Bug Fixes - **EVM Address Format Panic** — Fixed panic caused by invalid EVM address formatting during transaction decoding _(by @anhthii #21)_ - **Failed Transaction Filtering** — Filtered out failed EVM transactions to improve data accuracy and indexing reliability _(by @anhthii)_ - **General Refactoring & Cleanup** — Codebase refactoring and minor bug fixes for improved maintainability _(by @anhthii #12)_ --- ## Mpcium v0.3.3 **GitHub:** [fystack/mpcium](https://github.com/fystack/mpcium) ### Kubernetes & Docker Support - Added official Dockerfile and Kubernetes deployment guide for seamless containerized rollout - Improved setup scripts for cross-platform compatibility (bash, sed, etc.) ### Authorization Framework Enhancements - Introduced public-key-based authorization with algorithm selection and backward compatibility - Refactored initiator message authorization for clarity and consistency - Event consumer now verifies authorized signatures for improved message trust - Added detailed examples for generating authorizers and configuration setup ### Cloud Observability & Reliability - Added `/health` endpoint for cloud and Kubernetes probes - Improved context cancellation handling for graceful shutdowns - Added informative logs for Consul initialization - Refined configuration loading order and mandatory config validation ### Performance & Benchmarking - Introduced new benchmark command to measure signing and keygen performance - Refined benchmark logic for improved accuracy and developer insights ### Setup & Tooling Improvements - Enhanced setup scripts and configuration generation flow - Default CLI now reads `./peers.json` automatically (no manual path required) - Updated identity examples and directory structure documentation - Upgraded to latest Consul API for improved cluster compatibility ### Security & Stability Fixes - Fixed ECDH race condition in distributed deployments - Corrected S256 public key encoding issue - Improved error messages and minor cleanup for better debugging --- # v0.1.7 ## Release v0.1.7 — 2025-09-16 This release introduces TRON blockchain support, high availability (HA) implementation, enhanced security features, and significant improvements to messaging infrastructure and deployment workflows. --- ## New Features ### TRON Blockchain Support - Initialized TRON support across various components - Fixed TRON signing with MPC - Enhanced TRON transaction handling with new error checks and balance updates - Improved TRON address validation - Fixed TRON asset display issues - Added update address support for MPC wallets ### High Availability (HA) Implementation - Implemented leader election for HA - Added separate functions to start scheduler and block scanners - Optimized HA logic for distributed deployments - Added HA sample configuration - Improved graceful shutdown logic - Support for distributed mode in rescanner ### Security & Access Control - Implemented secret store supporting AWS and config reader - Moved encryption key to secret store - Added AWS KMS signer support - Added system admin checker - Implemented RBAC on wallet role assignment - Only allow system admin to update and add networks/assets - Improved webhook security - Added address risk checker implementation - Load TLS configuration from config files ### Messaging & Queue Infrastructure - Migrated statistic events to use queue - Migrated standard signing to queue - Migrated pubsub to queue consumer (#312) - Fixed #300: migrate pubsub to queue consumer - Marked NATS core pubsub as legacy - Added max age for JetStream configuration ### Email Service Integration - Added email service integration for user verification and payment notifications (#298) ### Aptos Enhancements - Enhanced Aptos fund sweeping functionality with wallet type support - Refactored transaction handling for Aptos FA - Improved error handling for Aptos operations --- ## Improvements ### Sweep Logic Enhancements - Major improvements to sweep logic - Implemented deterministic taskID for sweeps - Fixed sweep task conflicts - Fixed precision when updating balances - Implemented sweep batches in chunks - Improved sweep job logic ### Error Handling & Reliability - Enhanced error handling (#315) - Refactored MPC signing event for better error handling - Increased MPC signing withdrawal timeout - Enhanced robust automation logic - Fixed stack overflow issue - Improved payment service ### Configuration & Deployment - Used distroless to build Docker image for API (improved security) - Better logging setup - Updated Redis configuration - Support building multiple architectures - Refactored Docker Compose setup for APEX services - Added production scripts - Updated configuration to allow logging ### API & Endpoints - Added rescan API to rescan missing blocks - Added RPC health check script - Updated Consul version and configuration - Only add HTTP auth if both username and password are defined - Return 404 instead of 500 for webhook validation - Fixed #288: renamed reject to cancel for cancel API for withdrawal (#299) ### Rate Limiting - Made rate limit configurable --- ## Bug Fixes - Fixed MPC transaction broadcast for TRON - Fixed get Webacy configuration - Fixed add asset TRX assets not shown - Replaced dummy addresses with proper implementation - Removed root user migration for production - Fixed various typos and minor issues --- ## Developer & Internal Changes - Cleaned up codebase - Reverted HA changes on rescanner and scheduler (rescanner supports distributed mode, scheduler does not support multiple instances) - Code refactoring for leader logic - Moved start rescanner logic to initialize - Updated documentation - Removed hardcoded values - Discarded draft code - Reverted testnet seed assets - Updated Makefile - Refactored create webhook verification key - Refactored add asset to wallet logic - Updated wallet asset management for Hyper wallet - Added batch logic for statistic indexer --- ## Deployment Notes - **AWS KMS Integration**: If using AWS KMS for key management, ensure AWS credentials and KMS configuration are properly set - **Secret Store**: Update configuration to use secret store instead of direct config files for sensitive data - **Queue Migration**: NATS JetStream configuration required for queue-based messaging (pubsub is now legacy) - **Docker Images**: New distroless-based Docker images provide improved security - **Multi-Architecture**: Docker images now support multiple architectures (amd64, arm64) - **HA Deployment**: Use provided HA sample configuration for high availability setups - **TLS Configuration**: Ensure TLS certificates are configured if using encrypted connections - **System Admin**: First deployment requires creating a system admin user for network/asset management --- # v0.1.6 ## Release v0.1.6 — 2025-08-21 This release focuses on improving withdrawal workflows, enhancing webhook functionality, and adding new API endpoints for better workspace management. --- ## New Features ### Withdrawal Enhancements - Added wallet settings to store auto approval limits - Added more API endpoints and filters for withdrawals (#271) - Added more test cases for withdrawal service - Improved withdrawal validation ### Webhook Improvements - Implemented worker pool for webhook processing for better performance and scalability ### API Additions - Added Resend Invitation API endpoint and service logic (#272) - Enhanced wallet functionality with user and asset search capabilities (#267) - Added extra information for network responses --- ## Improvements - Changed status names of RPC health checks for better clarity - Upgraded resend package to latest version --- ## Deployment Notes - No special deployment requirements for this release - Webhook worker pool will automatically improve performance for high-volume webhook processing --- # v0.1.5 ## Release v0.1.5 — 2025-08-12 This release delivers major enhancements, new APIs, improved reliability, and extensive bug fixes across withdrawal workflows, sweeping, policies, and infrastructure. --- ## New Features ### Withdrawal APIs - Added `get-withdrawal-by-id` endpoint. - Added `request-withdrawal` API. - Added webhook callbacks for deposits and withdrawals. - Added API to get policy ID. - Added approval count and threshold in withdrawal approvals. - Implemented return withdrawal detail in approval response. ### Sweeper & Automation - Support sweep automation with address book. - Implemented robust deduplication for sweeper. - Support sweep task params in wallet creation. - Added retry mechanism for MPC signing in sweeper. - Experimental MPC sweeper integration. ### Service Accounts - Added endpoints for create, update, delete service accounts. - Implemented API endpoints for service account management (#225). ### Policy Management - Implemented create, approve, reject policy workflows. - Added rule validator for policy checks. - Enhanced policy validation readability. - Sync policy revision status. - Added approval workflow experimental implementation. ### Network & Asset Management - Integrated Monad network configuration. - Asset search now supports prefix matching (#252). - Added network repository & service integration (#253). - Improved wallet detail API with enriched fields. ### Infrastructure & Monitoring - Added CoinMarketCap price monitoring. - WebSocket auto-switch if no header for a period. - Added Consul check for availability. - Enhanced Redis stack setup with Lua script updates. - Support loading config from environment variables by default. --- ## Improvements - Improved not found block handling & rescanner performance. - Added relation filter to queries. - Separated notifications by workspace with workspaceId. - Disabled logging for failed retry attempts to reduce noise. - Improved error logging for failed WebSocket clients. - Added resiliency improvements for balance utilities. - Increased retry delay for RPC failover and adjusted thresholds. - Improved handling of nil Redis responses. - Made Solana scanner faster. - Updated Dockerfiles & deployment scripts for production readiness. - Moved email sender info to config file. - Removed deprecated HD key logic. --- ## Bug Fixes - Fixed withdrawal webhook issues & verification key authorization. - Fixed policy threshold condition. - Fixed whitelisted flag logic. - Fixed sweep task issues & crash when sweeping from MPC wallet. - Fixed sweep-to-address-book logic. - Fixed inconsistent policy query status. - Fixed user not found after being invited to workspace. - Fixed naming inconsistencies in APIs. - Fixed gas tank wallet existence check. - Fixed Solana rescanner logic. - Fixed special char encoding in email verification links. - Fixed workspaceId validation in raw transaction signing. - Fixed daily volume stats (changed day to 7). - Fixed approval re-approval prevention for withdrawals. - Fixed missing block store issues. --- ## Developer & Internal Changes - Added extra logs for MPC signing events. - Added retry logic for NATS messaging. - Enhanced error reason reporting for EVM transactor withdrawals. - Refactored withdrawal consumer for better maintainability. - Refactored missing block store & Lua scripts. - Cleaned up Docker, logs, and deployment scripts. - Removed some unused params from asset creation payload. - Added ignore tag in JSON serialization for user model. - Temporarily removed email verification requirement for sign-in. --- ## Deployment Notes - Requires updated environment variables for email sender config. - Run new database migrations (file-based migration added). - Update Docker images (migrate & main app) before deploying. - Ensure Redis stack container & Lua scripts are updated. - CoinMarketCap API key required for price monitoring feature. --- # Users The Users API exposes endpoints for retrieving the current signed-in user and working with notifications. Fystack does not expose general user CRUD endpoints under `/api/v1/users`. User registration, sign-in, MFA, passkeys, and session management are handled by the Authentication API. > **Info.** User management for workspace members is handled through workspace and role flows rather than generic user CRUD routes. --- ## Get current user **Endpoint:** `GET /users/me` Retrieve the user attached to the current access token. ### Authentication - `Authorization` (header): Bearer access token for the signed-in user. ### Response - `success` (boolean): Indicates whether the request was successful. - `message` (string): A message describing the result of the operation. - `code` (integer): A numeric code indicating the result status. - `data` (object): Contains the current user details. - `user_id` (string): The UUID of the user. - `email` (string): The user's email address. - `username` (string): The user's display name. - `is_2fa_enabled` (boolean): Whether MFA is enabled for the user. **Request** `GET /users/me` ```bash curl -X GET https://api.fystack.io/api/v1/users/me \ -H "Authorization: Bearer your_access_token" ``` ```json { "success": true, "message": "success", "code": 0, "data": { "user_id": "075243c1-962e-4803-83da-d73c78b8b060", "email": "devteam@fystack.io", "username": "Dev team", "expires_at": 1765852800, "is_2fa_enabled": false } } ``` --- ## Get user by session **Endpoint:** `GET /users/me/:session_id` Retrieve basic user details for an active sign-in session. This route is used during authentication flows before the user has completed all required steps. ### Path parameters - `session_id` (string): Session UUID returned by the sign-in flow. **Request** `GET /users/me/:session_id` ```bash curl -X GET https://api.fystack.io/api/v1/users/me/550e8400-e29b-41d4-a716-446655440000 ``` ```json { "success": true, "message": "success", "code": 0, "data": { "user_id": "075243c1-962e-4803-83da-d73c78b8b060", "email": "devteam@fystack.io", "username": "Dev team", "is_2fa_enabled": true, "session_id": "550e8400-e29b-41d4-a716-446655440000" } } ``` --- ## Get notifications **Endpoint:** `GET /users/:user_id/notifications` Retrieve notifications for the signed-in user. The requested `user_id` must match the user in the access token. ### Query parameters - `read` (boolean): Filter notifications by read state. - `offset` (integer): The number of notifications to skip. - `limit` (integer): Limit the number of notifications returned. The maximum value is 1000. **Request** `GET /users/:user_id/notifications` ```bash curl -X GET "https://api.fystack.io/api/v1/users/075243c1-962e-4803-83da-d73c78b8b060/notifications?read=false&limit=20&offset=0" \ -H "Authorization: Bearer your_access_token" ``` ```json { "success": true, "message": "success", "code": 0, "data": { "new_notification_count": 2, "notifications": [] } } ``` --- ## Check notification status **Endpoint:** `GET /users/:user_id/notifications/subscribe` Check whether the signed-in user has unread notifications. The requested `user_id` must match the user in the access token. **Request** `GET /users/:user_id/notifications/subscribe` ```bash curl -X GET https://api.fystack.io/api/v1/users/075243c1-962e-4803-83da-d73c78b8b060/notifications/subscribe \ -H "Authorization: Bearer your_access_token" ``` ```json { "success": true, "message": "success", "code": 0, "data": { "has_new": true } } ``` --- ## Mark notifications read **Endpoint:** `POST /users/notifications/read` Mark notifications as read for the signed-in user. ### Request body - `all` (boolean): When true, marks all notifications visible to the user as read. - `notification_ids` (array): Optional list of notification UUIDs to mark as read when `all` is false. **Request** `POST /users/notifications/read` ```bash curl -X POST https://api.fystack.io/api/v1/users/notifications/read \ -H "Authorization: Bearer your_access_token" \ -H "Content-Type: application/json" \ -d '{ "all": false, "notification_ids": ["9a85c312-7d3b-4c6f-9f4e-123456789abc"] }' ``` ```json { "success": true, "message": "success", "code": 0, "data": null } ``` --- ## Stream notifications **Endpoint:** `GET /users/:user_id/notifications/stream` Open a server-sent events stream for notification updates. The requested `user_id` must match the user in the access token. **Request** `GET /users/:user_id/notifications/stream` ```bash curl -N https://api.fystack.io/api/v1/users/075243c1-962e-4803-83da-d73c78b8b060/notifications/stream \ -H "Authorization: Bearer your_access_token" \ -H "Accept: text/event-stream" ```