Documentation Index
Fetch the complete documentation index at: https://docs.groundtech.co/llms.txt
Use this file to discover all available pages before exploring further.
This page covers the conventions shared across all Portfolio Wallet API endpoints.
Base path
The canonical API path is /v2/wallets. All wallet endpoints use this path prefix.
Authentication
All requests require a Bearer token in the Authorization header. If the token is missing or invalid, the API returns 401.
curl -X GET "https://sandbox.groundtech.co/v2/wallets" \
-H "Authorization: Bearer $API_TOKEN"
{
"data": [
{
"id": "9d1a1c83-3a1c-4c14-9c5a-0c9a57a4a7db",
"label": "Core Yield Portfolio",
"status": "idle",
"createdAt": "2026-02-05T08:15:00Z",
"depositAddresses": {
"arbitrum": "0x21246509968c4d24611f414560971AEc2e3A079B",
"base": "0x21246509968c4d24611f414560971AEc2e3A079B",
"ethereum": "0x21246509968c4d24611f414560971AEc2e3A079B",
"polygon": "0x21246509968c4d24611f414560971AEc2e3A079B",
"solana": "7nYzKxM3bP4oEFbqkPmA5E2rYJ8HqKz8vFg9abc1"
},
"balance": {
"totalUsd": "82500.000000",
"withdrawableUsd": "82500.000000",
"reservedUsd": "0.000000",
"earnedUsd": "250.000000"
},
"positions": [
{ "id": "syrup-usdc", "kind": "yield_source", "label": "Syrup USDC", "valueUsd": "33000.000000", "pct": 40 },
{ "id": "morpho-gauntlet-usdc", "kind": "yield_source", "label": "Morpho Gauntlet USDC Prime", "valueUsd": "24750.000000", "pct": 30 },
{ "id": "morpho-steakhouse-usdc", "kind": "yield_source", "label": "Morpho Steakhouse USDC Prime", "valueUsd": "24750.000000", "pct": 30 }
]
}
],
"nextCursor": null
}
Environments
| Environment | Base URL |
|---|
| Sandbox | https://sandbox.groundtech.co |
| Production | https://production.groundtech.co |
Sandbox focuses on test networks while you integrate. Production processes live funds on supported chains. Swap the base URL to move between environments, but note that sandbox uses explicit testnet chain keys such as ethereum_sepolia and may expose a smaller yield-source catalog than production.
Sandbox is limited to a subset of networks for integration testing. Unlike production, sandbox chain keys include the network suffix to make the testnet explicit:
ethereum_sepolia — Ethereum Sepolia testnet
Production uses canonical chain names (arbitrum, base, ethereum, polygon, solana). See Supported Chains for details.
List endpoints use cursor-based pagination with limit and cursor parameters.
curl -X GET "$BASE_URL/v2/wallets/$WALLET_ID/withdrawals?limit=25" \
-H "Authorization: Bearer $API_TOKEN"
The response includes a nextCursor field. Pass it as cursor to fetch the next page:
curl -X GET "$BASE_URL/v2/wallets/$WALLET_ID/withdrawals?limit=25&cursor=$NEXT_CURSOR" \
-H "Authorization: Bearer $API_TOKEN"
When nextCursor is null, you have reached the end of the result set.
Additional query parameters on list endpoints:
sort: field to sort by (e.g. createdAt)
order: desc (default) or asc
createdAtGte: ISO-8601 timestamp filter
status: repeatable status filter (e.g. status=processing&status=completed)
Idempotency
Create and withdrawal endpoints accept a requestId (UUID v4). These endpoints return 200 OK for both the initial request and an idempotent replay with the same requestId — the original response is returned unchanged on replay. If the requestId was already used with different parameters, the API returns 409 Conflict.
curl -X POST "$BASE_URL/v2/wallets" \
-H "Authorization: Bearer $API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"requestId": "f0a3afc3-1e67-4c0c-a8e1-3f5d8fb9a23b",
"label": "Core Yield Portfolio",
"strategy": {
"allocations": [
{ "yieldSourceId": "syrup-usdc", "pct": 40 },
{ "yieldSourceId": "morpho-gauntlet-usdc", "pct": 30 },
{ "yieldSourceId": "morpho-steakhouse-usdc", "pct": 30 }
]
}
}'
The canonical public write shape uses yieldSourceId and pct. Legacy positionKey / targetWeightBps request payloads remain accepted for backwards compatibility, but new integrations should use yield source IDs.
409 response:
{
"error": "Duplicate requestId",
"code": "duplicate_request_id"
}
When you receive a 409, fetch the existing resource by requestId rather than retrying the create.
Rate limiting
All /v2 endpoints are rate limited:
| Scope | Limit |
|---|
General (all /v2 endpoints) | 200 requests/min |
| Write endpoints (POST, PATCH, DELETE) | 20 requests/min |
Rate limit status is communicated via response headers:
| Header | Description |
|---|
RateLimit-Limit | Maximum requests allowed in the current window |
RateLimit-Remaining | Requests remaining in the current window |
RateLimit-Reset | Seconds until the rate limit window resets |
When rate limited, the API returns 429 Too Many Requests. Back off and retry after the RateLimit-Reset interval.
Error handling
Errors are returned as JSON with a human-readable error string and a machine-readable code field.
{
"error": "Missing required fields: destinationAddress, amountUsd, destinationChain, requestId",
"code": "validation_error"
}
| HTTP status | Meaning | Action |
|---|
400 | Validation error | Fix the request and retry |
401 | Authentication failed | Check your Bearer token |
404 | Resource not found | Verify the wallet/withdrawal ID |
409 | Duplicate requestId | Fetch the existing resource instead |
429 | Rate limited | Back off and retry after RateLimit-Reset |
500 | Server error | Log the response and retry with backoff |
All error responses consistently include both error and code fields. Use the code field for programmatic branching. Use the error string for logging and debugging.