> ## Documentation Index
> Fetch the complete documentation index at: https://docs.portalhq.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Request tracing

> Correlate Portal SDK requests with your own logs using the X-Portal-Trace-Id header.

Every request the Portal SDK makes to Portal's own APIs carries a trace ID. Portal's server-side logs are indexed by that value, so a single ID turns "a signing request failed sometime Tuesday" into an exact record. As of Android SDK `9.1.0` you can supply your own trace ID, log it next to your own request IDs, and hand it to Portal support.

## Overview

A trace ID is a UUID v4 sent as the `X-Portal-Trace-Id` HTTP header. The SDK attaches it to requests it makes to Portal-owned hosts, and maps it to the `reqId` field on MPC signing metadata so the same value covers both the API call and the MPC operation it triggers.

There are two ways a trace ID comes into existence:

* **The SDK generates one.** This is the default and requires no code changes.
* **You supply one.** Pass it through `RequestOptions`, `SendAssetParams`, or `rawSign` to stitch Portal's traces into your own observability.

## Automatic tracing

You do not have to do anything. When you do not supply a trace ID, the SDK generates a fresh UUID for each request. Tracing is always on, and there is no way to disable it.

## Which requests carry the header

The header is attached **only when the request targets a Portal-owned host**. The SDK checks the URL with `isPortalOwnedUrl` before setting the header, which is true for:

* Hosts containing `portalhq` — for example `api.portalhq.io` and `mpc.portalhq.io`
* Local development hosts — `localhost`, `*.localhost`, `127.0.0.1`, and `10.0.2.2` (the Android emulator's host loopback)

Everything else is excluded. In practice that means:

* **Third-party RPC endpoints** configured through your gateway config do **not** receive the header. If you inspect traffic to your own Alchemy or Infura endpoint and see no `X-Portal-Trace-Id`, that is expected behavior, not a bug.
* **Google Drive** requests made while storing or fetching a backup share do **not** receive the header.

<Note>
  Two consequences worth internalizing: you will not see the header on traffic to the third-party RPC providers you configure, and the SDK will not hand a correlation ID to them. The host check is a substring match on `portalhq`, so a custom host that happens to contain `portalhq` is treated as Portal-owned.
</Note>

`isPortalOwnedUrl` is public, so you can check a URL yourself:

```kotlin theme={null}
import io.portalhq.android.utils.isPortalOwnedUrl

val rpcUrl = "https://api.portalhq.io/rpc/v1/eip155/11155111"

if (isPortalOwnedUrl(rpcUrl)) {
    // This request will carry X-Portal-Trace-Id
    Log.i("Tracing", "Portal-owned host — trace header will be attached")
} else {
    Log.i("Tracing", "Third-party host — trace header will be omitted")
}
```

## Supplying your own trace ID

Three public entry points accept a trace ID.

### Provider requests

`RequestOptions` gained a `traceId` property:

```kotlin theme={null}
data class RequestOptions(
    val signatureApprovalMemo: String? = null,
    val sponsorGas: Boolean? = null,
    val traceId: String? = null,
)
```

```kotlin theme={null}
import io.portalhq.android.provider.data.EthTransactionParam
import io.portalhq.android.provider.data.PortalRequestMethod
import io.portalhq.android.provider.data.RequestOptions
import io.portalhq.android.storage.mobile.PortalNamespace
import io.portalhq.android.utils.generateTraceId

val traceId = generateTraceId()

val params = listOf(
    EthTransactionParam(
        from = portal.getAddress(PortalNamespace.EIP155)!!,
        to = "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb",
        gas = "0x6000",
        gasPrice = null,
        maxFeePerGas = null,
        maxPriorityFeePerGas = null,
        value = "0x1",
        data = "",
    ),
)

val result = portal.request(
    chainId = "eip155:11155111",
    method = PortalRequestMethod.eth_sendTransaction,
    params = params,
    options = RequestOptions(
        signatureApprovalMemo = "Send 1 wei",
        traceId = traceId,
    ),
)

Log.i("Signing", "Sent portalTraceId=$traceId txHash=${result.result}")
```

`RequestOptions.traceId` is **always** mapped to the MPC signing metadata `reqId`, even when the RPC URL itself is a third-party host that does not receive the header. That is the non-obvious payoff: one value correlates the API request and the MPC signing operation it triggers.

### Sending assets

`SendAssetParams` gained a `traceId` property:

```kotlin theme={null}
data class SendAssetParams(
    val to: String,
    val amount: String,
    val token: String,
    val signatureApprovalMemo: String? = null,
    val sponsorGas: Boolean? = null,
    val traceId: String? = null,
)
```

### Raw signing

`rawSign` gained a four-argument overload. The original three-argument signature is unchanged:

```kotlin theme={null}
suspend fun rawSign(
    message: String,
    chainId: String,
    signatureApprovalMemo: String? = null,
): PortalProviderResult

suspend fun rawSign(
    message: String,
    chainId: String,
    signatureApprovalMemo: String?,
    traceId: String?,
): PortalProviderResult
```

```kotlin theme={null}
import io.portalhq.android.utils.generateTraceId

val traceId = generateTraceId()

val result = portal.rawSign(
    message = "0x48656c6c6f",
    chainId = "eip155:11155111",
    signatureApprovalMemo = "Sign login challenge",
    traceId = traceId,
)

Log.i("RawSign", "Signature portalTraceId=$traceId signature=${result.result}")
```

## Multi-step flows share one ID

When an operation makes several requests, all of them carry the same trace ID.

`sendAsset` resolves one trace ID at the start of the call and reuses it for the build-transaction request, the MPC signing operation, and the broadcast. Pass `traceId` in `SendAssetParams` and the whole flow is correlated under your value:

```kotlin theme={null}
import io.portalhq.android.data.SendAssetParams
import io.portalhq.android.utils.generateTraceId

val traceId = generateTraceId()
Log.i("Transfer", "Starting transfer portalTraceId=$traceId")

portal.sendAsset(
    chainId = "eip155:11155111",
    params = SendAssetParams(
        to = "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb",
        amount = "0.01",
        token = "NATIVE",
        traceId = traceId,
    ),
).onSuccess { response ->
    Log.i("Transfer", "Submitted portalTraceId=$traceId txHash=${response.data?.txHash}")
}.onFailure { error ->
    Log.e("Transfer", "Failed portalTraceId=$traceId", error)
}
```

MPC wallet operations — `createWallet`, `backupWallet`, `recoverWallet`, and wallet ejection — and the EIP-7702 upgrade in `portal.evmAccountType.upgradeTo7702` also share a single trace ID across all of their sub-requests. These operations generate the ID internally and do not accept one from the caller.

<Tip>
  Generate one trace ID per user-facing action, not per request. A single ID spanning "user tapped Send" is what makes the correlation useful; a fresh ID per HTTP call tells you nothing you did not already know.
</Tip>

## Reading the ID back

`BackupWalletResponse` gained `shareIds` and `traceId`:

```kotlin theme={null}
data class BackupWalletResponse(
    val cipherText: String,
    val storageCallback: suspend () -> Result<Boolean>,
    val shareIds: List<String> = emptyList(),
    val traceId: String = "",
)
```

This is the **only** place the SDK hands a generated trace ID back to you. No other response type exposes one, so do not go looking for the same accessor elsewhere.

```kotlin theme={null}
import io.portalhq.android.mpc.data.BackupMethods

try {
    val backupResponse = portal.backupWallet(backupMethod = BackupMethods.Password)

    Log.i(
        "Backup",
        "Complete portalTraceId=${backupResponse.traceId} shares=${backupResponse.shareIds}",
    )
} catch (error: Throwable) {
    Log.e("Backup", "Backup failed", error)
}
```

<Note>
  `traceId` defaults to an empty string, not `null`. Check it with `isNotEmpty()` rather than a null check.
</Note>

## Helpers

The `io.portalhq.android.utils` package exposes the tracing primitives the SDK uses internally.

```kotlin theme={null}
const val PORTAL_TRACE_ID_HEADER = "X-Portal-Trace-Id"

fun generateTraceId(): String
fun resolveTraceId(traceId: String?): String
fun isPortalOwnedUrl(url: String): Boolean

fun Map<String, String>.traceIdHeaderValue(): String?
fun Map<String, String>.hasTraceIdHeader(): Boolean
```

| Helper                                     | Purpose                                                                                                                                      |
| ------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------- |
| `PORTAL_TRACE_ID_HEADER`                   | The header name, `"X-Portal-Trace-Id"`. Use the constant instead of hardcoding the string.                                                   |
| `generateTraceId()`                        | Generates a new UUID v4 trace ID in the format Portal expects.                                                                               |
| `resolveTraceId(traceId)`                  | Returns the supplied ID when it is non-blank, otherwise generates a fresh one. Use it when threading an optional ID through your own layers. |
| `isPortalOwnedUrl(url)`                    | Whether a URL will receive the header. Returns `false` for a malformed URL.                                                                  |
| `Map<String, String>.traceIdHeaderValue()` | Reads the trace ID out of a header map. The key lookup is case-insensitive and blank values are skipped.                                     |
| `Map<String, String>.hasTraceIdHeader()`   | Whether a header map already carries a non-blank trace ID.                                                                                   |

`resolveTraceId` exists so your own functions can take an optional trace ID and always have a concrete value to log:

```kotlin theme={null}
import io.portalhq.android.data.SendAssetParams
import io.portalhq.android.utils.resolveTraceId

suspend fun transfer(to: String, amount: String, traceId: String? = null) {
    val id = resolveTraceId(traceId) // The caller's ID, or a fresh one
    Log.i("Transfer", "portalTraceId=$id to=$to amount=$amount")

    portal.sendAsset(
        chainId = "eip155:11155111",
        params = SendAssetParams(
            to = to,
            amount = amount,
            token = "NATIVE",
            traceId = id,
        ),
    ).onFailure { error ->
        Log.e("Transfer", "Failed portalTraceId=$id", error)
    }
}
```

The two `Map` extensions are for code that builds or inspects header maps directly — an OkHttp interceptor that stamps your own outbound requests with the same ID, for example:

```kotlin theme={null}
import io.portalhq.android.utils.PORTAL_TRACE_ID_HEADER
import io.portalhq.android.utils.hasTraceIdHeader
import io.portalhq.android.utils.resolveTraceId
import io.portalhq.android.utils.traceIdHeaderValue
import okhttp3.Interceptor
import okhttp3.Response

class TraceIdInterceptor(private val traceId: String) : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        val request = chain.request()
        val headers = request.headers.toMap()

        if (headers.hasTraceIdHeader()) {
            Log.i("Tracing", "Already traced: ${headers.traceIdHeaderValue()}")
            return chain.proceed(request)
        }

        val traced = request.newBuilder()
            .header(PORTAL_TRACE_ID_HEADER, resolveTraceId(traceId))
            .build()

        return chain.proceed(traced)
    }
}
```

## Sharing a trace ID with Portal support

Log the trace ID alongside your own request IDs so that when something goes wrong you can look up both sides of the same event. Include the ID in support tickets — it is the fastest way for Portal to find the exact request.

<Warning>
  A trace ID is not a secret, but it is a correlation handle. Do not log it next to user PII in a system with a different retention policy than your Portal logs.
</Warning>

## Signature changes in 9.1.0

Threading `traceId` through the transport and API layers touched a lot of signatures. Almost none of it is breaking.

<Note>
  If you only **call** Portal's APIs from Kotlin, `9.1.0` requires no changes. Every `Api` and `EvmAccountTypeApi` method that gained a `traceId` parameter did so through an overload, or — for `ejectClient` and `storedClientBackupShare` — through a default value plus `@JvmOverloads`, so those original signatures survive in both source and bytecode. `PortalRequests` is the exception: its methods gained a defaulted `traceId` *without* `@JvmOverloads`, so Kotlin source callers are unaffected but the pre-`9.1.0` JVM descriptors are not preserved — see [Transport layer](#transport-layer) below.
</Note>

### The overload-pair pattern

Most `Api` methods kept their original arity and gained a new one alongside it:

```kotlin theme={null}
open suspend fun getClient(): ClientResponse
open suspend fun getClient(traceId: String?): ClientResponse

open suspend fun getClientCipherText(backupSharePairId: String): String
open suspend fun getClientCipherText(backupSharePairId: String, traceId: String?): String

open suspend fun prepareEject(walletId: String, backupMethod: BackupMethods): String
open suspend fun prepareEject(walletId: String, backupMethod: BackupMethods, traceId: String?): String

open suspend fun refreshClient()
open suspend fun refreshClient(traceId: String?)

open suspend fun storeClientCipherText(backupSharePairId: String, cipherText: String): Boolean
open suspend fun storeClientCipherText(backupSharePairId: String, cipherText: String, traceId: String?): Boolean

open suspend fun updateSharePairStatus(
    type: PortalMpcShareType,
    status: PortalMpcShareStatus,
    sharePairIds: List<String>,
): Boolean
open suspend fun updateSharePairStatus(
    type: PortalMpcShareType,
    status: PortalMpcShareStatus,
    sharePairIds: List<String>,
    traceId: String?,
): Boolean

open suspend fun buildEip155Transaction(
    chainId: String,
    params: BuildTransactionParam,
): Result<BuildEip115TransactionResponse>
open suspend fun buildEip155Transaction(
    chainId: String,
    params: BuildTransactionParam,
    traceId: String?,
): Result<BuildEip115TransactionResponse>

open suspend fun buildSolanaTransaction(
    chainId: String,
    params: BuildTransactionParam,
): Result<BuildSolanaTransactionResponse>
open suspend fun buildSolanaTransaction(
    chainId: String,
    params: BuildTransactionParam,
    traceId: String?,
): Result<BuildSolanaTransactionResponse>
```

`EvmAccountTypeApi` follows the same pattern for `getStatus`, `buildAuthorizationList`, and `buildAuthorizationTransaction`.

`EvmAccountTypePortalDependency` — the small interface you implement if you supply your own Portal dependency to `EvmAccountType` — gained a defaulted overload, so existing implementations still compile:

```kotlin theme={null}
suspend fun rawSign(
    message: String,
    chainId: String,
    signatureApprovalMemo: String?,
    traceId: String?,
): PortalProviderResult = rawSign(message, chainId, signatureApprovalMemo)
```

### Two methods changed in place

`ejectClient` and `storedClientBackupShare` gained `traceId` **in place** with a default value rather than through a hand-written overload:

```kotlin theme={null}
@JvmOverloads
open fun ejectClient(traceId: String? = null): Result<Boolean>

@JvmOverloads
fun storedClientBackupShare(
    success: Boolean,
    backupMethod: BackupMethods,
    traceId: String? = null,
): Result<Boolean>
```

Both carry `@JvmOverloads`, so the compiler also emits the original zero-`traceId` arity into the bytecode. The `9.0.x` JVM descriptors are preserved, which means existing calls keep resolving and no recompile is required:

```kotlin theme={null}
// Both of these still compile and still resolve against 9.1.0
portal.api.ejectClient()
portal.api.storedClientBackupShare(true, BackupMethods.Password)
```

### Transport layer

Every `PortalRequests` method gained a trailing defaulted `traceId`:

```kotlin theme={null}
open suspend fun delete(url: URL, bearerToken: String? = null, traceId: String? = null): String
open suspend fun get(url: URL, bearerToken: String? = null, traceId: String? = null): String
open suspend fun patch(url: URL, bearerToken: String? = null, payload: String, traceId: String? = null): String
open suspend fun post(url: URL, bearerToken: String? = null, payload: String?, traceId: String? = null): String
open suspend fun put(url: URL, bearerToken: String? = null, payload: String?, traceId: String? = null): String
```

<Warning>
  This is the one change in `9.1.0` that can break a build. These methods changed in place rather than gaining an overload, so if you subclass `PortalRequests` to stub the transport in tests, your existing `override` fails with `'get' overrides nothing.` until you add the `traceId` parameter.

  Because there is no `@JvmOverloads` here, the pre-`9.1.0` JVM method descriptors are gone as well. Kotlin source callers are fine — the default value covers them — but a binary compiled against `9.0.x` that calls these methods directly will not link against `9.1.0`. Recompile any such module rather than dropping the new SDK in beside it.
</Warning>

```kotlin theme={null}
import io.portalhq.android.utils.PortalRequests
import java.net.URL

class FakeRequests : PortalRequests() {
    // Before 9.1.0: override suspend fun get(url: URL, bearerToken: String?): String
    override suspend fun get(url: URL, bearerToken: String?, traceId: String?): String {
        return """{"result":"stubbed"}"""
    }
}
```

### Other affected types

| Type                   | Change                                                                                                |
| ---------------------- | ----------------------------------------------------------------------------------------------------- |
| `RequestOptions`       | Gained `traceId`, plus a secondary constructor matching the pre-`9.1.0` argument list.                |
| `SendAssetParams`      | Gained `traceId` as a trailing defaulted property.                                                    |
| `BackupWalletResponse` | Gained `shareIds` and `traceId`, plus a secondary constructor matching the pre-`9.1.0` argument list. |
| `MpcMetadata`          | Gained `reqId`, serialized as `"reqId"`.                                                              |
| `MpcSigner.sign`       | Gained a trailing `reqId: String? = null`, written onto the signing metadata.                         |

## Next Steps

* Turn on SDK logs to see requests as they happen — [Configure log level](./configure-log-level)
* Learn how Portal surfaces failures — [Error handling](./error-handling)
* Review the signing methods that accept a trace ID — [Sign a transaction](./sign-a-transaction)
* Call Portal's APIs directly — [Portal API methods](./portal-api-methods)
