Stablecoin Off-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.
Stablecoin-to-USD conversions are triggered via Payment Order type: book transfers . Use webhooks to track lifecycle events.
Recommended implementation order
Build in this sequence:
- Onboard end users by creating Legal Entities and waiting for KYC/KYB approval.
- Open USD and stablecoin Internal Accounts for each approved entity.
- Register counterparties with external bank accounts for fiat and wallet addresses for stablecoin.
- Build the off-ramp: receive stablecoins, convert to USD, and pay out via a fiat rail.
- 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.
{
"event": "activated",
"data": {
"id": "<LEGAL_ENTITY_ID>",
"status": "active",
"legal_entity_type": "business"
}
}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 C2 USD 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 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 the blockchain addresses you use to receive stablecoins:
curl --request GET \
-u ORGANIZATION_ID:API_KEY \
--url https://app.moderntreasury.com/api/internal_accounts/<ORG_USDC_INTERNAL_ACCOUNT_ID>{
"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 off-ramp flow, create a fiat counterparty for the customer:
Create a bank counterparty
curl --request POST \
-u ORGANIZATION_ID:API_KEY \
--url https://app.moderntreasury.com/api/counterparties \
-H 'content-type: application/json' \
-d '{
"name": "Acme Payments",
"legal_entity_id": "<CUSTOMER_ENTITY_ID>",
"accounts": [
{
"account_type": "checking",
"routing_details": [
{
"routing_number_type": "aba",
"routing_number": "<ROUTING_NUMBER>"
}
],
"account_details": [
{ "account_number": "<ACCOUNT_NUMBER>" }
]
}
]
}'4. Off-ramp: receive stablecoins, convert to USD, and pay out
Complete the off-ramp in four steps: receive stablecoins, convert them to USD, wait for settlement, and send the fiat payout.
Step A: Receive stablecoins
Your stablecoin Internal Account has a unique blockchain address for each supported network. External parties can send stablecoins directly to those addresses.
To retrieve the blockchain addresses:
curl --request GET \
-u ORGANIZATION_ID:API_KEY \
--url https://app.moderntreasury.com/api/internal_accounts/<ORG_USDC_INTERNAL_ACCOUNT_ID>Share the relevant address with your counterparty. Inbound transfers appear as Incoming Payment Detail to your stablecoin Internal Account.
{
"event": "completed",
"data": {
"id": "<INCOMING_PAYMENT_DETAIL_ID>",
"object": "incoming_payment_detail",
"type": "stablecoin",
"amount": 1000000,
"currency": "USDC",
"direction": "credit",
"status": "completed",
"internal_account_id": "<ORG_USDC_INTERNAL_ACCOUNT_ID>",
"originating_account_number": "0x1234567890abcdef1234567890abcdef12343f9a",
"originating_account_number_safe": "3f9a",
"originating_account_number_type": "ethereum_address"
}
}Step B: Convert stablecoins to USD (book transfer)
Represent stablecoin-to-USD conversions as book transfers 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": "USD",
"originating_account_id": "<ORG_USDC_INTERNAL_ACCOUNT_ID>",
"receiving_account_id": "<ORG_USD_INTERNAL_ACCOUNT_ID>"
}'Note: Off-ramp orchestration currently supports book transfers from a C1 stablecoin Internal Account to a C1 USD Internal Account. Create one additional Payment Order to move funds to a Customer (C2) Internal Account.
See the flow-of-funds diagram at the top of this section.
Step C: Wait for settlement
Wait for the payment_order.completed webhook before you initiate the fiat payout:
{
"event": "completed",
"data": {
"id": "<PAYMENT_ORDER_ID>",
"object": "payment_order",
"type": "book",
"status": "completed"
}
}Step D: Pay out via fiat rail
After the payment order completes, send USD to the counterparty's external bank 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": "credit",
"currency": "USD",
"originating_account_id": "<CUST_USD_INTERNAL_ACCOUNT_ID>",
"receiving_account_id": "<BANK_EXTERNAL_ACCOUNT_ID>"
}'Use wire or rtp instead of ach if you need a different speed or cost profile. rtp payments route automatically over RTP or FedNow. See Originating Payments for settlement timing and pre-funding requirements.
Updated 14 days ago