Skip to main content
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",
      "createdAt": "2026-02-05T08:15:00Z",
      "depositAddresses": {
        "arbitrum": "0x21246509968c4d24611f414560971AEc2e3A079B",
        "base": "0x21246509968c4d24611f414560971AEc2e3A079B",
        "ethereum": "0x21246509968c4d24611f414560971AEc2e3A079B",
        "polygon": "0x21246509968c4d24611f414560971AEc2e3A079B",
        "solana": "7nYzKxM3bP4oEFbqkPmA5E2rYJ8HqKz8vFg9abc1"
      },
      "balance": {
        "totalUsd": "82500.000000",
        "withdrawableUsd": "82500.000000",
        "availableToInitiateUsd": "82500.000000",
        "pendingWithdrawalUsd": "0.000000",
        "inTransitUsd": "0.000000",
        "earnedUsd": "250.000000"
      },
      "positions": [
        { "yieldSourceId": "syrup-usdc", "name": "Syrup USDC", "valueUsd": "33000.000000", "pct": 40 },
        { "yieldSourceId": "morpho-gauntlet-usdc", "name": "Morpho Gauntlet USDC Prime", "valueUsd": "24750.000000", "pct": 30 },
        { "yieldSourceId": "morpho-steakhouse-usdc", "name": "Morpho Steakhouse USDC Prime", "valueUsd": "24750.000000", "pct": 30 }
      ]
    }
  ],
  "nextCursor": null
}

Environments

EnvironmentBase URL
Sandboxhttps://sandbox.groundtech.co
Productionhttps://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.

Pagination

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). Retrying the same request with the same requestId returns the original response: 201 for the initial creation and 200 for an idempotent 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:
ScopeLimit
General (all /v2 endpoints)200 requests/min
Write endpoints (POST, PATCH, DELETE)20 requests/min
Rate limit status is communicated via response headers:
HeaderDescription
RateLimit-LimitMaximum requests allowed in the current window
RateLimit-RemainingRequests remaining in the current window
RateLimit-ResetSeconds 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 statusMeaningAction
400Validation errorFix the request and retry
401Authentication failedCheck your Bearer token
404Resource not foundVerify the wallet/withdrawal ID
409Duplicate requestIdFetch the existing resource instead
429Rate limitedBack off and retry after RateLimit-Reset
500Server errorLog 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.