Stablecoin On-ramp

Stablecoin payments use the same primitives as fiat:

  • Legal Entities to onboard end users
  • Internal Accounts to hold USD and stablecoin balances
  • Counterparties to represent external wallets and bank accounts
  • Payment Orders to move funds across rails.

USD-to-stablecoin conversions are triggered via Payment Order type: book transfers . Use webhooks to track lifecycle events.


Recommended implementation order

Build in this sequence:

  1. Onboard end users by creating Legal Entities and waiting for KYC/KYB approval.
  2. Open USD and stablecoin Internal Accounts for each approved entity.
  3. Register counterparties with external bank accounts for fiat and wallet addresses for stablecoin.
  4. Build the on-ramp: fund a USD internal account, convert to stablecoins, and send them to an external wallet.
  5. Subscribe to webhooks for Payment Order and Legal Entity lifecycle events.

1. Onboard end users

Every entity that holds funds on your platform needs a verified Legal Entity. For required fields and documents, see:

Create a business or individual Legal Entity

The example below creates a business Legal Entity with a control person. In production, also include beneficial owners, required documents (articles of incorporation and proof of address), and any additional identifications. See the onboarding guide above for the complete field list.

curl --request POST \
  -u ORGANIZATION_ID:API_KEY \
  --url https://app.moderntreasury.com/api/legal_entities \
  -H 'content-type: application/json' \
  -d '{
    "legal_entity_type": "business",
    "business_name": "Acme Payments Inc.",
    "legal_structure": "corporation",
    "business_description": "Online marketplace for handmade goods",
    "addresses": [
      {
        "address_types": ["business"]
        "primary": true,
        "line1": "100 Market Street",
        "locality": "San Francisco",
        "region": "CA",
        "postal_code": "94105",
        "country": "US"
      }
    ],
    "date_formed": "2020-06-15",
    "expected_activity_volume": 100000000,
    "intended_use": "Accepting payments from international customers",
    "identifications": [
      {
        "id_type": "us_ein",
        "issuing_country": "US",
        "id_number": "123456789"
      }
    ],
    "documents": [
      {
        "document_type": "proof_of_address",
        "file_data": "<base64 encoded bytes>",
        "filename": "proof_of_address.pdf"
      }
    ]
  }'

Wait for approval

Wait for the activated webhook before you open accounts. In production, Modern Treasury may manually review legal entities, so ask your account manager for expected SLAs. In sandbox, legal entities activate automatically.

2. Open accounts

After the Legal Entity is approved, create Internal Accounts for USD and stablecoin balances. Each Internal Account is a programmable sub-account with its own account and routing number (for USD) or blockchain address (for stablecoins).

You will provision one USD Internal Account and one stablecoin Internal Account for the end user. The USD account will be created using your customer's legal_entity_id and the stablecoin account (USDC, USDG, etc.) will be created using your own organization's legal_entity_id:

Create a USD Internal Account for your customer

curl --request POST \
  -u ORGANIZATION_ID:API_KEY \
  --url https://app.moderntreasury.com/api/internal_accounts \
  -H 'content-type: application/json' \
  -d '{
    "name": "Acme Payments USD Account",
    "party_name": "Acme Payments Inc.",
    "currency": "USD",
    "legal_entity_id": "<CUSTOMER_LEGAL_ENTITY_ID>"
  }'

Retrieved Organization (C1) Legal Entity ID

Use your Organization Legal Entity ID to provision the stablecoin Internal Account:

curl --request GET \
  -u ORGANIZATION_ID:API_KEY \
  --url 'https://app.moderntreasury.com/api/legal_entities?business_name=<ORGANIZATION_NAME>' \
  -H 'content-type: application/json'

These details are also available within the legal entities UI in the Modern Treasury app.

Create a stablecoin Internal Account

curl --request POST \
  -u ORGANIZATION_ID:API_KEY \
  --url https://app.moderntreasury.com/api/internal_accounts \
  -H 'content-type: application/json' \
  -d '{
    "name": "Acme Payments USDC Account",
    "party_name": "Acme Payments Inc.",
    "currency": "USDC",
    "legal_entity_id": "<ORGANIZATION_ENTITY_ID>"
  }'

The stablecoin Internal Account response includes blockchain addresses you can use to receive stablecoins:

{
    "id": "<ORG_USDC_INTERNAL_ACCOUNT_ID>",
    "object": "internal_account",
    "party_name": "Acme Payments Inc.",
    "name": "Acme Payments USDC Account",
    "currency": "USDC",
    "legal_entity_id": "5d95643d-1127-4a7c-9ef5-ad21a1d007c6",
    "account_details": [
        {
            "account_number": "<ETHEREUM_ADDRESS>",
            "account_number_type": "ethereum_address"
        },
        {
            "account_number": "<BASE_ADDRESS>",
            "account_number_type": "base_address"
        },
        {
            "account_number": "<POLYGON_ADDRESS>",
            "account_number_type": "polygon_address"
        },
        {
            "account_number": "<ARBITRUM_ONE_ADDRESS>",
            "account_number_type": "arbitrum_one_address"
        },
        {
            "account_number": "<SOLANA_ADDRESS>",
            "account_number_type": "solana_address"
        },
        {
            "account_number": "<STELLAR_ADDRESS>",
            "account_number_type": "stellar_address"
        }
    ],
    "status": "active"
}

For the full list of supported stablecoins and blockchain networks, see Stablecoins.


3. Set up counterparties

For the on-ramp flow, create a wallet counterparty for the external stablecoin wallet you will send to:

Create a wallet counterparty

curl --request POST \
  -u ORGANIZATION_ID:API_KEY \
  --url https://app.moderntreasury.com/api/counterparties \
  -H 'content-type: application/json' \
  -d '{
    "name": "Vendor Wallet",
    "accounts": [
      {
        "account_details": [
          {
            "account_number": "<COUNTERPARTY_WALLET_ADDRESS>",
            "account_number_type": "ethereum_address"
          }
        ]
      }
    ]
  }'

For the full list of supported account_number_type values, see Account Detail.

If you also need a bank counterparty for fiat funding, see Create a Counterparty.


4. On-ramp: fund USD internal account, convert to stablecoins, and send

On-ramp flow diagram

Complete the on-ramp in three steps: fund a USD Internal Account, convert USD to stablecoins, and send the stablecoins to an external wallet.

Step A: Fund the USD Internal Account

There are two funding patterns depending on who initiates the transfer.

Pull (you initiate the debit). Create a Payment Order that pulls funds from the external party's bank account into your USD Internal Account:

curl --request POST \
  -u ORGANIZATION_ID:API_KEY \
  --url https://app.moderntreasury.com/api/payment_orders \
  -H 'content-type: application/json' \
  -d '{
    "type": "ach",
    "amount": 100000,
    "direction": "debit",
    "currency": "USD",
    "originating_account_id": "<CUST_USD_INTERNAL_ACCOUNT_ID>",
    "receiving_account_id": "<BANK_EXTERNAL_ACCOUNT_ID>"
  }'

Push (sender credits your account). The sender initiates the transfer to your USD Internal Account. No Payment Order is created on your side — the funds arrive as an inbound payment. This pattern applies to ACH credit, wire, and RTP / FedNow.

Share your USD Internal Account's routing and account numbers with the sender. Retrieve them from the Internal Account object:

curl --request GET \
  -u ORGANIZATION_ID:API_KEY \
  --url https://app.moderntreasury.com/api/internal_accounts/<USD_INTERNAL_ACCOUNT_ID>

Wait for the completed webhook before proceeding to the conversion step. The webhook you receive depends on the funding pattern:

{
  "event": "completed",
  "data": {
    "id": "<PAYMENT_ORDER_ID>",
    "object": "payment_order",
    "type": "ach",
    "status": "completed"
  }
}
{
  "event": "completed",
  "data": {
    "id": "<INCOMING_PAYMENT_DETAIL_ID>",
    "object": "incoming_payment_detail",
    "type": "ach",
    "status": "completed",
    "amount": 100000,
    "currency": "USD",
    "direction": "credit",
    "internal_account_id": "<CUST_USD_INTERNAL_ACCOUNT_ID>"
  }
}

Settlement timing varies by rail. See Originating Payments for details on each payment type.

💡

Simulation: To test inbound fiat funding events in Sandbox, simulate an Incoming Payment Detail at Simulate Incoming Payment Detail.

Step B: Convert USD to stablecoins (book transfer)

Once the USD has settled, convert it to stablecoins with a book transfer between Internal Accounts.

curl --request POST \
  -u ORGANIZATION_ID:API_KEY \
  --url https://app.moderntreasury.com/api/payment_orders \
  -H 'content-type: application/json' \
  -d '{
    "type": "book",
    "amount": 100000,
    "direction": "credit",
    "currency": "USDC",
    "originating_account_id": "<ORG_USD_INTERNAL_ACCOUNT_ID>",
    "receiving_account_id": "<ORG_USDC_INTERNAL_ACCOUNT_ID>"
  }'

Wait for the payment_order.completed webhook before you send the stablecoins:

{
  "event": "completed",
  "data": {
    "id": "<PAYMENT_ORDER_ID>",
    "object": "payment_order",
    "type": "book",
    "status": "completed"
  }
}
💡

Note: On-ramp orchestration currently supports book transfers between C1 USD and C1 stablecoin Internal Accounts. One additional payment order is needed to transfer funds from a Customer (C2) Internal Account.

Step C: Send stablecoins to an external wallet

After the conversion settles, send stablecoins to the counterparty's external wallet:

curl --request POST \
  -u ORGANIZATION_ID:API_KEY \
  --url https://app.moderntreasury.com/api/payment_orders \
  -H 'content-type: application/json' \
  -d '{
    "type": "stablecoin",
    "amount": 100000,
    "direction": "credit",
    "currency": "USDC",
    "originating_account_id": "<ORG_USDC_INTERNAL_ACCOUNT_ID>",
    "receiving_account_id": "<WALLET_EXTERNAL_ACCOUNT_ID>"
  }'
💡

amount uses the coin's smallest unit as an integer. For example, USDC uses 6 decimals, so 10000000 = 10.00 USDC. Stablecoin wallet addresses are validated and screened before payment execution.

The blockchain network is determined by the account_number_type on the receiving counterparty's external account. A counterparty created with account_number_type: "ethereum_address" will route the payment to an EVM-compatible network. To send on Base, Solana, Polygon PoS, or another chain, register the counterparty wallet address with the matching account_number_type. Your Modern Treasury account team can help configure network routing during onboarding.

For the full list of supported stablecoins and blockchain networks, see Stablecoins.


If you use ACH debits for funding, Modern Treasury offers Accelerated ACH Debit Settlement to settle faster than the standard two days.