Skip to main content

Subscribe & Redeem

Subscribe Flow

The subscription process involves both backend API calls and on-chain contract interactions:

┌──────────┐ ① GET /earn/products ┌──────────────┐
│ Client │ ───────────────────────▶ │ Public API │
│ │ ◀─────────────────────── │ │
│ │ Product list └──────────────┘
│ │
│ │ ② POST /earn/subscribe/prepare
│ │ ───────────────────────▶ ┌──────────────┐
│ │ ◀─────────────────────── │ Protected API│
│ │ EIP-712 signature └──────────────┘
│ │
│ │ ③ approve USDT
│ │ ───────────────────────▶ ┌──────────────┐
│ │ │ USDT Contract│
│ │ ④ call subscribe() └──────────────┘
│ │ ───────────────────────▶ ┌──────────────┐
│ │ │ Earn Contract│
└──────────┘ └──────┬───────┘
│ ⑤ Subscribed Event

┌──────────────┐
│ Backend │
│ Creates record│
│ Mints NFT │
└──────────────┘

Step 1: Get EIP-712 Domain

GET /earn/domain

Response

{
"types": {
"EIP712Domain": [
{ "name": "name", "type": "string" },
{ "name": "version", "type": "string" },
{ "name": "chainId", "type": "uint256" },
{ "name": "verifyingContract", "type": "address" }
],
"Subscribe": [
{ "name": "user", "type": "address" },
{ "name": "productId", "type": "uint256" },
{ "name": "amount", "type": "uint256" },
{ "name": "deadline", "type": "uint256" }
]
},
"primaryType": "Subscribe",
"domain": {
"name": "ZTDX Earn",
"version": "1",
"chainId": 421614,
"verifyingContract": "0x436dcB8a2478D636a6cC678AE7A2E4c5449cB3ba"
}
}

Step 2: Browse Products

GET /earn/products

Query Parameters

ParameterTypeRequiredDescription
statusstringNoFilter by status: created / subscribing / active / settled / ended / cancelled
pagenumberNoPage number, default 1
page_sizenumberNoItems per page, default 20

Example

GET /earn/products?status=subscribing&page=1&page_size=10

Response

{
"products": [
{
"id": "123204f6-5bde-4434-890a-8ca241f66067",
"chain_product_id": 1769524310,
"name": "USDT Earn 900% APY",
"description": null,
"annual_rate": "900.00%",
"period_rate": "0.00%",
"annual_rate_bps": 90000,
"period_rate_bps": 0,
"duration_seconds": 300,
"total_quota": "500000000.00",
"subscribed_amount": "500000000.00",
"available_quota": "0.00",
"subscription_rate": "100.00%",
"min_amount": "100000000.00",
"max_amount_per_user": "500000000.00",
"status": "subscribing",
"subscriber_count": 1,
"subscribe_start_time": "2026-01-27T14:33:00Z",
"subscribe_end_time": "2026-01-27T14:38:00Z",
"settle_time": "2026-01-27T14:43:00Z",
"is_subscribing": true,
"is_sold_out": false,
"time_remaining_seconds": 180
}
],
"total": 1,
"page": 1,
"page_size": 20
}

Product Fields (ProductDetail)

FieldTypeDescription
idstringProduct UUID
chain_product_idnumberOn-chain product ID
namestringProduct name
descriptionstring | nullProduct description
annual_ratestringAnnual yield (percentage string)
period_ratestringPeriod yield (percentage string)
annual_rate_bpsnumberAnnual rate in basis points (1% = 100)
period_rate_bpsnumberPeriod rate in basis points
duration_secondsnumberLock duration in seconds
total_quotastringTotal subscription cap (Wei)
subscribed_amountstringTotal subscribed amount (Wei)
available_quotastringRemaining available quota (Wei)
subscription_ratestringSubscription progress percentage
min_amountstringMinimum subscription amount (Wei)
max_amount_per_userstringMaximum per-user subscription (Wei)
statusstringProduct status
subscriber_countnumberNumber of subscribers
subscribe_start_timestringSubscription start time (ISO 8601)
subscribe_end_timestringSubscription end time (ISO 8601)
settle_timestringSettlement time (ISO 8601)
is_subscribingbooleanWhether currently in subscription period
is_sold_outbooleanWhether fully subscribed
time_remaining_secondsnumber | nullSeconds remaining in subscription window (only during subscription period)

Step 3: Get Product Details

GET /earn/products/:id
ParameterTypeDescription
idstringProduct UUID or chain_product_id (integer string)

Returns a ProductDetail object (same fields as product list).

Step 4: Prepare Subscription (Get Signature)

Authentication Required

This endpoint requires a valid JWT token in the Authorization header:

Authorization: Bearer <JWT_TOKEN>
POST /earn/subscribe/prepare

Request Body

FieldTypeRequiredDescription
product_idstringYesProduct UUID
amountstringYesSubscription amount in human-readable USDT (e.g., "100" = 100 USDT)
{
"product_id": "123204f6-5bde-4434-890a-8ca241f66067",
"amount": "100"
}

Response

FieldTypeDescription
chain_product_idnumberOn-chain product ID
amountstringSubscription amount in Wei (for contract call)
deadlinenumberSignature expiry (Unix timestamp, 30 min from now)
signaturestringEIP-712 backend signature
contract_addressstringEarn contract address
user_addressstringUser wallet address
{
"chain_product_id": 1769524310,
"amount": "100000000",
"deadline": 1772006400,
"signature": "0x1a2b3c...",
"contract_address": "0x436dcB8a2478D636a6cC678AE7A2E4c5449cB3ba",
"user_address": "0x29F721B203A9fC9c5DDe35A739d8b8E0E4605489"
}

Subscription Restrictions

ConditionError Code
Product not in subscribing statusSUBSCRIPTION_CLOSED
Current time outside subscription windowSUBSCRIPTION_CLOSED
Amount below minimumAMOUNT_TOO_LOW
Exceeds per-user maximumAMOUNT_TOO_HIGH
Insufficient remaining quotaQUOTA_EXCEEDED
Invalid amount formatINVALID_AMOUNT
Zero or negative amountINVALID_AMOUNT

Step 5: On-Chain Subscription

After obtaining the signature, the client must execute two on-chain transactions:

// Step 1: Approve the Earn contract to spend USDT
await usdtContract.approve(contract_address, amount); // amount in Wei

// Step 2: Call subscribe on the Earn contract
await earnContract.subscribe(
chain_product_id, // On-chain product ID
amount, // Wei amount (from response)
deadline, // From response
signature // From response
);
Interest Calculation Example

900% APY, 300s lock (≈5 min), 100 USDT subscription:

Interest = 100 × 9.00 × (300 / 31536000) ≈ 0.0000855 USDT


Redeem (Claim)

After a product reaches settled status, users can claim principal + interest directly on-chain. No backend API call is needed.

Claim Principal + Interest

// Claim after product is settled (NFT is automatically burned)
await earnContract.claim(chain_product_id);

The backend listens for the Claimed event and automatically updates the subscription record:

  • nft_statusredeemed
  • claimedtrue
  • claim_tx_hash is recorded

Emergency Refund

When a product is in cancelled status, users can get their principal back:

// Emergency refund for cancelled products
await earnContract.emergencyClaim(chain_product_id);
note

Claiming and emergency refund are purely on-chain operations. The backend automatically synchronizes state via event listeners (polling interval ~12 seconds). Subscription records may take 12–30 seconds to reflect the updated status.


Code Examples

Python — Browse Products (Public)

import requests

BASE_URL = "https://api.ztdx.io"

# Get EIP-712 domain info
resp = requests.get(f"{BASE_URL}/earn/domain")
print("Domain:", resp.json())

# List subscribing products
resp = requests.get(f"{BASE_URL}/earn/products", params={"status": "subscribing", "page": 1, "page_size": 10})
data = resp.json()
for p in data["products"]:
print(f" {p['name']} — APY: {p['annual_rate']}, Quota: {int(p['available_quota']) / 1e6:.2f} USDT")

# Get product details
product_id = data["products"][0]["id"] if data["products"] else "example-uuid"
resp = requests.get(f"{BASE_URL}/earn/products/{product_id}")
print("Product detail:", resp.json())

Python — Prepare Subscription (JWT Required)

import requests

BASE_URL = "https://api.ztdx.io"
JWT_TOKEN = "your_jwt_token"

resp = requests.post(
f"{BASE_URL}/earn/subscribe/prepare",
headers={"Authorization": f"Bearer {JWT_TOKEN}", "Content-Type": "application/json"},
json={"product_id": "123204f6-5bde-4434-890a-8ca241f66067", "amount": "100"},
)
data = resp.json()
print(f"Chain Product ID: {data['chain_product_id']}")
print(f"Amount (Wei): {data['amount']}, Deadline: {data['deadline']}")
print(f"Signature: {data['signature']}")
# Use this signature to call earnContract.subscribe() on-chain

Error Codes

Error CodeHTTP StatusDescription
SUBSCRIPTION_CLOSED400Product not in subscription period (status or time window)
AMOUNT_TOO_LOW400Amount below product minimum
AMOUNT_TOO_HIGH400Exceeds per-user maximum limit
QUOTA_EXCEEDED400Insufficient remaining product quota
INVALID_AMOUNT400Invalid amount format, zero, or negative
PRODUCT_NOT_FOUND404Product ID does not exist
UNAUTHORIZED401Not authenticated or invalid token

Error Response Format

{
"error": "Product is not open for subscription",
"code": "SUBSCRIPTION_CLOSED"
}