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

# Manage smart sessions from a backend

> Register a backend remote access credential, reconcile owner-approved sessions, and execute bounded EVM transactions with the TypeScript SDK.

Smart sessions let an application backend submit transactions from a user's wallet without asking the owner to approve every transaction. The wallet owner approves an explicit set of EVM transfer grants and an expiry first. The wallet's Sessions Module enforces those limits on-chain.

The backend API is available through the TypeScript SDK's `RemoteAccessClient`. The owner can approve or revoke the session with any OMS Wallet SDK. In this guide, **client app** means any web or mobile frontend that integrates an OMS Wallet SDK to add wallet functionality.

<Note>
  Smart sessions currently support EVM wallets. Keep the remote access credential (RAC) private key on the backend. Any operator or application client should call your authenticated backend API and must never receive or use that key.
</Note>

## Divide responsibilities

| Component | Responsibility |
| - | - |
| Client app | Authenticates the owner, displays the RAC's public metadata and requested grants, and asks the owner to authorize or revoke a session. |
| Application backend | Owns the RAC signer, calls `RemoteAccessClient`, stores any application workflow records, and exposes authenticated endpoints when another client needs to request or inspect operations. |
| Operator or admin client (optional) | Displays backend state and requests actions through the application backend. It does not sign WaaS requests. |
| WaaS and the Sessions Module | Provide the authoritative session, grant, and usage state and enforce the approved grants on-chain. |

If your application uses approval links, request statuses, operator authentication, or local transaction history, those are application features. They are not OMS Wallet SDK APIs.

## Understand the lifecycle

<Steps>
  <Step title="Register the backend credential">
    The backend creates or loads a persistent signer and registers it with WaaS. Registration returns a `credentialId`.
  </Step>

  <Step title="Ask the owner to approve">
    Your application sends the owner the `credentialId` and the requested network, grants, and expiry through its own approval flow.
  </Step>

  <Step title="Authorize in the client app">
    The owner inspects the credential metadata and calls `authorizeRemoteAccess`. The SDK returns the authoritative `walletId` and `sessionId`.
  </Step>

  <Step title="Associate the session with the backend">
    Associate the returned `walletId` and `sessionId` through your application workflow. The client can send them through an authenticated endpoint, or the backend can reconcile its current sessions with `listSessions`. In either case, verify the authoritative session with `getSession` and store an application association only if your workflow needs one.
  </Step>

  <Step title="Reconcile and operate">
    The backend reads the session from WaaS, checks its current grants and usage, and prepares and executes transactions within those limits.
  </Step>
</Steps>

## Create a persistent backend signer

Load the RAC private key from backend-only secret storage and construct one `RemoteAccessClient`. This Node-style example reads environment variables; use your platform's secret bindings in a Worker or other serverless runtime.

```typescript theme={null}
import {
  EthereumPrivateKeyCredentialSigner,
  RemoteAccessClient,
} from '@polygonlabs/oms-wallet'
import { hexToBytes, type Hex } from 'viem'

const publishableKey = process.env.OMS_PUBLISHABLE_KEY
const privateKey = process.env.OMS_RAC_PRIVATE_KEY

if (!publishableKey) {
  throw new Error('OMS_PUBLISHABLE_KEY is required')
}
if (!privateKey || !/^0x[0-9a-fA-F]{64}$/.test(privateKey)) {
  throw new Error('OMS_RAC_PRIVATE_KEY must be a 0x-prefixed 32-byte private key')
}

const credentialSigner = new EthereumPrivateKeyCredentialSigner(
  hexToBytes(privateKey as Hex),
)
const remoteAccess = new RemoteAccessClient({
  publishableKey,
  credentialSigner,
})
```

The signer supplies a strictly increasing nonce for every signed WaaS request. The built-in signer uses a time-based in-memory nonce and is suitable when one long-running process issues requests sequentially. A distributed or restart-sensitive deployment should implement the public `CredentialSigner` interface with a shared atomic nonce store. Requests for one RAC must still reach WaaS in increasing nonce order, so serialize their dispatch even when allocation is atomic.

## Register the credential

Register the signer and persist the returned ID:

```typescript theme={null}
const lifetimeSeconds = 30 * 24 * 60 * 60

const { credentialId } = await remoteAccess.registerCredential({
  lifetimeSeconds,
  metadata: {
    appName: 'Example automation backend',
    appUrl: 'https://example.com',
    appLogoUrl: 'https://example.com/logo.png',
    custom: {},
  },
})
```

When first registering a newly generated signer, store its managed-key reference, `credentialId`, registration time, and requested lifetime. `registerCredential` returns only the credential ID. Registering the same active signer again is idempotent and does not extend its original lifetime, so do not treat a later call as a renewal. A session cannot expire after its RAC; persist the effective `approved.expiresAt` returned to the client app.

The metadata is public consent-screen information returned to the client app by `inspectRemoteCredential`. Do not put secrets in it.

## Collect owner approval

Your application decides how to transport the approval request to the client app. In the client app, inspect the credential before asking the owner to authorize it. This runs in the client app: `credentialId` comes from the backend, and `approved.sessionId` goes back to it.

```typescript theme={null}
import { Networks } from '@polygonlabs/oms-wallet'

const metadata = await omsWallet.wallet.inspectRemoteCredential({ credentialId })

// Render this metadata and the exact requested grant in your consent UI.
console.log(metadata.appName, metadata.appUrl)

const approved = await omsWallet.wallet.authorizeRemoteAccess({
  credentialId,
  network: Networks.amoy,
  expiresAt: new Date(Date.now() + 60 * 60 * 1_000).toISOString(),
  grants: [
    {
      kind: 'nativeTransfer',
      to: '0x1111111111111111111111111111111111111111',
      limit: 1_000_000_000_000_000n,
    },
  ],
})

// Pass these values into your application's session-association workflow.
console.log(approved.walletId, approved.sessionId, approved.expiresAt)
```

The public SDK supports `nativeTransfer` and `erc20Transfer` grants. To replace the grants or expiry of a specific existing session without changing its signer, call `authorizeRemoteAccess` again and pass that session's `sessionId`.

The example uses Polygon Amoy. Grant limits and transaction values are raw base-unit amounts.

See the owner-side sessions guide for [TypeScript](/wallets/sdk/typescript/sessions-and-access#authorize-remote-access), [React Native](/wallets/sdk/react-native/sessions-and-access#authorize-remote-access), [Swift](/wallets/sdk/swift/sessions-and-access#authorize-remote-access), or [Kotlin](/wallets/sdk/kotlin/sessions-and-access#authorize-remote-access).

## Persist application state separately

Treat any session identifiers received from a client as untrusted application input. Before accepting an association, call `getSession` with the RAC and verify that the returned session ID, chain, grants, and effective expiry are consistent with the intended authorization.

Keep application workflow state separate from authoritative session state:

| Store in your application | Read from WaaS |
| - | - |
| Optional approval-request token, requested grants, requester, and workflow status | Active session ID, wallet ID, signer address, chain ID, grants, and expiry |
| Association between `credentialId`, `walletId`, `sessionId`, and your user or account | Current grant usage for a selected network |
| Prepared transaction ID and your audit or display history | Current transaction status |

If the client app also sends a wallet address, use it only for display or Indexer queries. Use the `walletId` returned by `getSession` when preparing a remote transaction.

## Reconcile application state

`listSessions` follows all WaaS pages and returns the sessions currently available to this RAC. Read usage for the session's network when showing remaining allowances:

```typescript theme={null}
import { findNetworkById } from '@polygonlabs/oms-wallet'

const sessions = await remoteAccess.listSessions({ pageSize: 50 })

for (const session of sessions) {
  const network = findNetworkById(session.chainId)
  if (!network) {
    continue
  }

  const usage = await remoteAccess.getSessionUsage({
    sessionId: session.sessionId,
    network,
  })

  console.log(session.sessionId, session.grants, usage)
}
```

Keep these RAC-signed reads sequential for a given credential. Your database can retain expired or revoked associations as application history, but do not present them as usable merely because a local record still exists.

## Prepare and execute a transaction

Resolve the session immediately before preparing the transaction. This gives the backend its authoritative wallet ID and current grants. This runs on the backend, with the `sessionId` the client app returned:

```typescript theme={null}
import { findNetworkById } from '@polygonlabs/oms-wallet'

const session = await remoteAccess.getSession({ sessionId })
const network = findNetworkById(session.chainId)
if (!network) {
  throw new Error(`Unsupported session chain: ${session.chainId}`)
}

const prepared = await remoteAccess.prepareTransaction({
  walletId: session.walletId,
  sessionId: session.sessionId,
  network,
  to: '0x1111111111111111111111111111111111111111',
  value: 100_000_000_000_000n,
})

// Persist prepared.txnId before asking WaaS to execute it.
await remoteAccess.executeTransaction({
  txnId: prepared.txnId,
})

const latest = await remoteAccess.getTransactionStatus({
  txnId: prepared.txnId,
})
```

Smart-session transactions must be sponsored by the relayer. WaaS rejects preparation when the transaction is not sponsored, so remote execution does not select or submit a fee option.

Validate the requested operation against the current grants and usage before your backend accepts it for execution. This provides clear errors and defense in depth, but the on-chain Sessions Module remains the final authority. Persist `txnId` before execution so an uncertain response can be reconciled with `getTransactionStatus` instead of blindly preparing or executing another transaction.

## Send native tokens to a per-call recipient

A `nativeTransfer` grant spends the network's native token, such as ETH or POL, and binds the session to the single `to` address the owner approves. Every prepared transaction must send to that same address. An `erc20Transfer` grant can omit its recipient because its permission targets the token contract and the recipient is only a calldata argument of `transfer`. The native-transfer recipient cannot be optional: the Sessions Module enforces the grant as an on-chain permission bound to one call target, and a permission that accepted any target would allow arbitrary contract calls, not only value transfers. See the [public grant model](/wallets/smart-sessions-deep-dive#public-grant-model) for how the grant types are enforced.

To choose the recipient per call without widening the session beyond value transfers, bind the grant to the `ValueForwarder` contract and pass the actual recipient in the calldata. [ValueForwarder](https://github.com/0xsequence/wallet-contracts-v3/blob/master/test/mocks/ValueForwarder.sol) is deployed at `0xABAAd93EeE2a569cF0632f39B10A9f5D734777ca` on the networks in the SDK's [network registry](/wallets/sdk/typescript/networks-and-reference), and its single function, `forwardValue(address to, uint256 value)`, sends `value` to `to` with empty calldata.

```typescript theme={null}
import { Networks } from '@polygonlabs/oms-wallet'
import { encodeFunctionData, parseAbi } from 'viem'

const valueForwarder = '0xABAAd93EeE2a569cF0632f39B10A9f5D734777ca'
const valueForwarderAbi = parseAbi([
  'function forwardValue(address to, uint256 value)',
])

// In the client app: bind the native-transfer grant to the forwarder.
await omsWallet.wallet.authorizeRemoteAccess({
  credentialId,
  network: Networks.amoy,
  expiresAt: new Date(Date.now() + 60 * 60 * 1_000).toISOString(),
  grants: [
    {
      kind: 'nativeTransfer',
      to: valueForwarder,
      // Cumulative 0.001 POL limit in wei.
      limit: 1_000_000_000_000_000n,
    },
  ],
})

// On the backend: choose the recipient and amount per call.
const amount = 100_000_000_000_000n

const prepared = await remoteAccess.prepareTransaction({
  walletId: session.walletId,
  sessionId: session.sessionId,
  network,
  to: valueForwarder,
  value: amount,
  data: encodeFunctionData({
    abi: valueForwarderAbi,
    functionName: 'forwardValue',
    args: ['0x2222222222222222222222222222222222222222', amount],
  }),
})

// Execute prepared.txnId with executeTransaction, as in the previous section.
```

Pass the same amount as the transaction `value` and the `forwardValue` argument; the forwarder sends on the argument amount, so any excess call value would remain held by the forwarder. The grant's cumulative limit still bounds the total value across calls, and a failing recipient call, such as a transfer to a contract without a payable `receive` function, reverts the whole transaction.

<Warning>
  The consent screen displays the forwarder address as the grant recipient, not the addresses the backend later chooses. Present the grant to the owner as native transfers to any address up to the approved limit. The forwarded call carries empty calldata, so a contract recipient runs only its receive or fallback logic, exactly as with a native transfer sent directly.
</Warning>

## Revoke or rotate access

The wallet owner revokes one specific session:

```typescript theme={null}
await omsWallet.wallet.revokeAccess({
  credentialId,
  sessionId,
})
```

The backend can retire the RAC and every session authorized for it:

```typescript theme={null}
await remoteAccess.revokeCredential({ credentialId })
```

Credential rotation requires a new private key, a new `RemoteAccessClient`, and a new registration. Sessions authorized for the retired credential are not transferable to the replacement credential; owners must approve new sessions.

## Production checklist

* Encrypt the RAC private key at rest or keep it in a managed secret or signing service. Never expose it outside the secure backend.
* Authenticate and authorize every caller of backend session-management endpoints. Add rate limits and abuse controls to any public approval endpoints.
* If you use approval links, treat their tokens as bearer credentials. Store only hashes, or encrypt them and strictly limit access.
* Use a shared atomic nonce allocator and serialize WaaS requests when more than one process can use the same RAC.
* Schedule requested session expiries before the planned RAC rotation, persist each effective approval expiry, and rotate credentials before they expire.
* Re-read the authoritative session and usage before presenting or executing an action.
* Persist each prepared `txnId` before execution and reconcile its status before retrying.
* Use the session's authoritative `walletId` for remote transactions. A wallet address alone is not sufficient.

<Card title="Smart session example" href="https://github.com/0xPolygon/oms-wallet-typescript-sdk/tree/master/examples/smart-session">
  See a complete Worker, database, owner approval app, and admin dashboard that implement this lifecycle.
</Card>
