Exchange Auth Intent

Exchange a settled auth intent for a JWT
View as Markdown

Exchange for JWT

After the agent pays the auth challenge, exchange the intent for a JWT:

1import { Gwop } from "@gwop/sdk";
2
3const gwop = new Gwop({
4 merchantApiKey: process.env.GWOP_MERCHANT_API_KEY,
5});
6
7const { result: token } = await gwop.authIntents.exchange({
8 authIntentId: "ai_mn5e49p01TJe5YUJ-Cw",
9 idempotencyKey: crypto.randomUUID(),
10});
11
12console.log(token.accessToken); // RS256-signed JWT
13console.log(token.principal.sub); // "base:0x742d..." or "solana:7sSi..."
14console.log(token.account.isNewAccount); // true on first auth

Parameters

FieldTypeRequiredDescription
authIntentIdstringYesThe authIntentId from authIntents.create()
idempotencyKeystringNoUUID v4 for safe retries

Response

The response contains:

FieldDescription
accessTokenRS256-signed JWT for authenticating subsequent requests
principal.subWallet identity: {chain}:{address}
account.isNewAccounttrue if this is the wallet’s first authentication

JWT sub format

The sub claim identifies the wallet:

base:0x742d35Cc6634C0532925a3b844Bc9e7595f5bA16
solana:7sSi2XK9pJuqMV9p4Lz3kxBRtxYRPcC5Yp7CYGkaFqJ

Parse it to extract the chain and address:

1const [chain, address] = token.principal.sub.split(":");
2// chain = "base", address = "0x742d..."

Errors

1import { ErrorCode, isGwopError } from "@gwop/sdk/errors";
2
3try {
4 await gwop.authIntents.exchange({ authIntentId, idempotencyKey: crypto.randomUUID() });
5} catch (err) {
6 if (isGwopError(err, ErrorCode.AuthIntentNotSettled)) {
7 // Agent hasn't paid yet — poll with backoff
8 } else if (isGwopError(err, ErrorCode.AuthIntentNotFound)) {
9 // Invalid intent ID — create a new one
10 } else if (isGwopError(err, ErrorCode.AuthIntentExpired)) {
11 // TTL exceeded — create a new auth intent
12 } else if (isGwopError(err, ErrorCode.AuthIntentUsed)) {
13 // Already exchanged — use the JWT from the first exchange
14 } else {
15 throw err; // Don't swallow unexpected errors
16 }
17}

HTTP 402 (Payment Required) is the correct status for AuthIntentNotSettled — the agent hasn’t paid the auth challenge. Your backend should poll and retry with backoff until the agent completes payment or the intent expires.

Next step