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
| Parameter | Type | Required | Description |
|---|---|---|---|
status | string | No | Filter by status: created / subscribing / active / settled / ended / cancelled |
page | number | No | Page number, default 1 |
page_size | number | No | Items 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)
| Field | Type | Description |
|---|---|---|
id | string | Product UUID |
chain_product_id | number | On-chain product ID |
name | string | Product name |
description | string | null | Product description |
annual_rate | string | Annual yield (percentage string) |
period_rate | string | Period yield (percentage string) |
annual_rate_bps | number | Annual rate in basis points (1% = 100) |
period_rate_bps | number | Period rate in basis points |
duration_seconds | number | Lock duration in seconds |
total_quota | string | Total subscription cap (Wei) |
subscribed_amount | string | Total subscribed amount (Wei) |
available_quota | string | Remaining available quota (Wei) |
subscription_rate | string | Subscription progress percentage |
min_amount | string | Minimum subscription amount (Wei) |
max_amount_per_user | string | Maximum per-user subscription (Wei) |
status | string | Product status |
subscriber_count | number | Number of subscribers |
subscribe_start_time | string | Subscription start time (ISO 8601) |
subscribe_end_time | string | Subscription end time (ISO 8601) |
settle_time | string | Settlement time (ISO 8601) |
is_subscribing | boolean | Whether currently in subscription period |
is_sold_out | boolean | Whether fully subscribed |
time_remaining_seconds | number | null | Seconds remaining in subscription window (only during subscription period) |
Step 3: Get Product Details
GET /earn/products/:id
| Parameter | Type | Description |
|---|---|---|
id | string | Product UUID or chain_product_id (integer string) |
Returns a ProductDetail object (same fields as product list).
Step 4: Prepare Subscription (Get Signature)
This endpoint requires a valid JWT token in the Authorization header:
Authorization: Bearer <JWT_TOKEN>
POST /earn/subscribe/prepare
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
product_id | string | Yes | Product UUID |
amount | string | Yes | Subscription amount in human-readable USDT (e.g., "100" = 100 USDT) |
{
"product_id": "123204f6-5bde-4434-890a-8ca241f66067",
"amount": "100"
}
Response
| Field | Type | Description |
|---|---|---|
chain_product_id | number | On-chain product ID |
amount | string | Subscription amount in Wei (for contract call) |
deadline | number | Signature expiry (Unix timestamp, 30 min from now) |
signature | string | EIP-712 backend signature |
contract_address | string | Earn contract address |
user_address | string | User wallet address |
{
"chain_product_id": 1769524310,
"amount": "100000000",
"deadline": 1772006400,
"signature": "0x1a2b3c...",
"contract_address": "0x436dcB8a2478D636a6cC678AE7A2E4c5449cB3ba",
"user_address": "0x29F721B203A9fC9c5DDe35A739d8b8E0E4605489"
}
Subscription Restrictions
| Condition | Error Code |
|---|---|
Product not in subscribing status | SUBSCRIPTION_CLOSED |
| Current time outside subscription window | SUBSCRIPTION_CLOSED |
| Amount below minimum | AMOUNT_TOO_LOW |
| Exceeds per-user maximum | AMOUNT_TOO_HIGH |
| Insufficient remaining quota | QUOTA_EXCEEDED |
| Invalid amount format | INVALID_AMOUNT |
| Zero or negative amount | INVALID_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
);
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_status→redeemedclaimed→trueclaim_tx_hashis 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);
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 Code | HTTP Status | Description |
|---|---|---|
SUBSCRIPTION_CLOSED | 400 | Product not in subscription period (status or time window) |
AMOUNT_TOO_LOW | 400 | Amount below product minimum |
AMOUNT_TOO_HIGH | 400 | Exceeds per-user maximum limit |
QUOTA_EXCEEDED | 400 | Insufficient remaining product quota |
INVALID_AMOUNT | 400 | Invalid amount format, zero, or negative |
PRODUCT_NOT_FOUND | 404 | Product ID does not exist |
UNAUTHORIZED | 401 | Not authenticated or invalid token |
Error Response Format
{
"error": "Product is not open for subscription",
"code": "SUBSCRIPTION_CLOSED"
}