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

# x402 Prepaid Wallets

> Assign one or more prepaid wallets to an AI agent and enforce principal-controlled spend policy through official x402 v2 payment messages.

Grantex connects delegated agent authority to prepaid value. A human principal
can assign one or more wallets to an agent, apply assignment, wallet, agent,
budget-group, principal, and developer controls, restrict semantic payment
context, approve exact exceptions or reloads, and stop one wallet or every
wallet immediately. The x402 adapter uses the official x402 v2
`PAYMENT-REQUIRED`, `PAYMENT-SIGNATURE`, and `PAYMENT-RESPONSE` headers.

<Note>
  The prepaid-wallet API and SDK shown here are available in the published
  `@grantex/sdk@0.8.2`, `@grantex/x402@0.4.1`, Python `grantex==0.7.2`, and Go
  `v0.4.3`. All four versions are registry verified. TypeScript `0.7.1`, Python
  `0.6.1`, and Go `v0.4.1` include bounded refresh recovery and retry hardening.
  `sandbox_ledger` is a complete local ledger mode.
  `external` wallet records fail closed until a custody/provider adapter verifies
  funding and settlement.
</Note>

## Payment network compatibility

The default remains `exact` on `grantex:prepaid`. The source checkout now also
supports opt-in Base native USDC EIP-3009. Automatic Base 402/sign/retry requires
published TypeScript `0.8.2` and x402 `0.4.1`, both registry verified.
Published Python `0.7.2` and Go `v0.4.3` include EVM payment responses
and authenticated reconciliation, but no automatic HTTP payment wrapper. Server
deployment and funded custody/RPC provisioning remain separate requirements.

Configure `baseUsdc: { scope: 'licensing:preflight' }` on `createX402Agent`,
supply a stable `idempotencyKey`, and provision the server's `base_usdc` custody
provider. Policies and balance are checked before a durable EVM signature is
returned. Merchants receive standard x402 fields, not a Grantex JWT, and need
no `extra.grantexScope` change. See [Base USDC custody](/guides/base-usdc-custody)
for setup, funding, finality and retries. Other chains/assets, Permit2, smart
wallets and Solana remain unsupported; unconfigured custody fails closed.

<Warning>
  Self-hosting the auth service does not create an external custody rail, reload
  notification channel, or merchant result store. Before production, complete
  the [Prepaid Wallet Production Readiness](/guides/prepaid-wallet-production)
  checklist. In particular, route the exact public `/v1/prepaid-wallets`
  audience to the auth service, keep `external` custody fail-closed until a real
  adapter is installed, bridge wallet events to the principal's chosen channel,
  and make merchant side effects idempotent under the HTTP `Idempotency-Key`.
</Warning>

## Security model

| Control | Enforcement point |
| - | - |
| Agent identity | OAuth Agent Grants access token, sender-constrained with DPoP |
| Wallet ownership | Principal-session JWT and developer/principal tenant partition |
| Per-transaction limit | Atomic authorization transaction in PostgreSQL |
| Rolling cumulative limit | Sum of reserved and settled amounts in the configured rolling window |
| Cross-wallet and cross-agent budgets | Layered policy lock plus reservation-aware amount and count windows |
| Balance | Available value moves to reserved before an authorization is issued |
| Merchant and action | Exact recipient, resource URL, scope, asset, network, merchant, purpose, project, and cost-center binding |
| Exceptional payment | Short-lived principal approval bound to the exact request and consumed once |
| Replay | Agent/principal-wide idempotency key plus a one-time reservation and authorization JTI |
| Stop control | Blocks prevent new signatures. Local-ledger holds are released; signed EVM exposure remains held until finalized settlement or expiry |
| Reload | Agent may request only at or below the principal's low-balance threshold; principal approves and funds separately |

Amounts are positive integer strings in the asset's smallest unit. For a wallet
with `decimals: 6`, `"1250000"` represents 1.25 units. Floating-point money is
not accepted.

## 1. Authorize the agent

Register the agent with the wallet scopes and the exact wallet resource:

```ts theme={null}
import { generateOAuthAgentKey, OAuthAgentClient } from '@grantex/sdk';

const key = await generateOAuthAgentKey();
// Register publicJwk, redirect URI, scopes, and resource server through the
// normal developer API before creating the client.
const oauth = await OAuthAgentClient.create({
  issuer: 'https://grantex.dev',
  clientId: agentId,
  redirectUri: 'https://agent.example/callback',
  resource: 'https://grantex.dev/v1/prepaid-wallets',
  privateKey: key.privateKey,
  publicJwk: key.publicJwk,
});

const pending = await oauth.beginAuthorization({
  scopes: [
    'wallet:read',
    'wallet:spend',
    'wallet:reload:request',
    'travel:book', // the exact action later carried in extra.grantexScope
  ],
  principalHint: principalId,
});
```

The access token audience must be exactly `/v1/prepaid-wallets`. The service
rejects bearer-token downgrade, a mismatched DPoP key, a revoked grant, and an
incorrect audience.

## 2. Create, fund, and assign wallets

The application creates a short-lived principal session after the principal has
an active grant. The human-facing wallet client uses that session, not the
developer API key.

```ts theme={null}
import { PrincipalPrepaidWalletClient } from '@grantex/sdk';

const principal = new PrincipalPrepaidWalletClient({
  baseUrl: 'https://api.grantex.dev',
  sessionToken,
});

const travel = await principal.create({
  name: 'Travel float',
  custodyMode: 'sandbox_ledger',
  network: 'grantex:prepaid',
  asset: 'USDC',
  decimals: 6,
  lowBalanceThreshold: '10000000',
});

await principal.reload(travel.walletId, '100000000', crypto.randomUUID());
const assignment = await principal.assign(travel.walletId, {
  agentId,
  budgetGroup: 'travel-2026',
  perTransactionLimit: '5000000',
  cumulativeLimit: '25000000',
  cumulativePeriodSeconds: 86400,
  allowedRecipients: ['merchant:travel-api'],
  allowedScopes: ['travel:book'],
  allowedResourceOrigins: ['https://merchant.example'],
});

await principal.createSpendPolicy({
  name: 'Travel group monthly budget',
  scopeType: 'group',
  scopeId: 'travel-2026',
  effect: 'limit',
  maxAmount: '100000000',
  windowType: 'month',
  onExceed: 'require_approval',
  purposes: ['business-travel'],
});
```

Repeat the create/assign steps to give one agent multiple wallets. An agent may
pin a wallet. When it does not, Grantex evaluates assigned wallets in stable
order and reserves against the first eligible wallet. An idempotent retry is
always pinned to its original reservation and cannot drift to another wallet.

## 3. Use official x402 v2

```ts theme={null}
import { PrepaidWalletAgentClient } from '@grantex/sdk';
import { createX402Agent } from '@grantex/x402';

const walletAgent = new PrepaidWalletAgentClient({
  oauthClient: oauth,
  accessToken: tokens.access_token,
});

const x402 = createX402Agent({
  authorizePayment: walletAgent.x402Authorizer,
  // Optional. Omit this to let Grantex choose an eligible assigned wallet.
  walletId: travel.walletId,
});

const logicalPaymentId = 'travel_01JZ8Y6Q2M4N7P9T';
const response = await x402.fetch('https://merchant.example/travel/quote', {
  // Forwarded to the merchant for durable post-settlement result recovery.
  headers: { 'Idempotency-Key': logicalPaymentId },
  // Consumed by Grantex for pre-settlement reservation recovery.
  idempotencyKey: logicalPaymentId,
});
```

The merchant's x402 v2 requirement must use:

```json theme={null}
{
  "scheme": "exact",
  "network": "grantex:prepaid",
  "amount": "1250000",
  "asset": "USDC",
  "payTo": "merchant:travel-api",
  "maxTimeoutSeconds": 120,
  "extra": {
    "grantexScope": "travel:book",
    "grantexContext": {
      "merchantId": "merchant:travel-api",
      "purpose": "business-travel",
      "projectId": "conference-2026",
      "costCenter": "sales"
    }
  }
}
```

`maxTimeoutSeconds` may be 1 through 300. The signed authorization binds the
wallet, assignment, agent, principal, grant, amount, asset, network, recipient,
resource URL, scope, merchant ID, purpose, project ID, cost center, and canonical
request hash. Changing any requirement makes verification fail.

The merchant controls this x402 requirement. Grantex does not trust a caller to
lower price, change the payee, or invent semantic context after the 402 response.
Issuer-side KYC, sanctions, fraud, card, MCC, geography, settlement, and dispute
controls remain separate and must also approve the transaction.

Remote resource URLs must use HTTPS. Plain HTTP is accepted only for loopback
development hosts such as `localhost`, `127.0.0.1`, and `[::1]`.

The DPoP access token must contain both `wallet:spend` and the exact
`extra.grantexScope` value. Assignment policy is an additional restriction; it
cannot elevate a scope omitted from the human-approved OAuth grant.

## 4. Handle exact payment approval

```ts theme={null}
import { PrepaidPaymentApprovalRequiredError } from '@grantex/x402';

try {
  await x402.fetch('https://merchant.example/travel/quote', {
    idempotencyKey: logicalPaymentId,
  });
} catch (error) {
  if (!(error instanceof PrepaidPaymentApprovalRequiredError)) throw error;

  // Notify the principal and wait for an approve or reject decision.
  await waitForPrincipal(error.approval.approvalRequestId);

  await x402.fetch('https://merchant.example/travel/quote', {
    walletId: error.approval.walletId,
    idempotencyKey: error.idempotencyKey,
    approvalRequestId: error.approval.approvalRequestId,
  });
}
```

The retry is accepted only after approval and only for the exact original terms.
Approval cannot be transferred to another wallet, recipient, amount, resource,
or semantic context.

## 5. Reload workflow

The agent can request value but cannot approve or fund its own request:

```ts theme={null}
const request = await walletAgent.requestReload(
  travel.walletId,
  '50000000',
  'reload_01JZ8Y6Q2M4N7P9T',
  'Balance reached the travel threshold',
);

await principal.decideReload(request.reloadRequestId, 'approved');
await principal.fundReload(request.reloadRequestId);
```

Reload requests are available only while the wallet is active, the assignment
is active, the agent is not globally blocked, and available value is at or below
the configured threshold. Persist and reuse the idempotency key for every retry.
The exact request is returned even if the principal already approved, rejected,
or funded it. Changed terms conflict, and a different key cannot create a second
pending request for the same agent and wallet.

## 6. Principal stop controls

```ts theme={null}
// Stop only this agent/wallet relationship.
await principal.setAssignmentStatus(assignment.assignmentId, 'blocked', 'Trip ended');

// Stop every agent assigned to one wallet.
await principal.setWalletStatus(travel.walletId, 'blocked', 'Wallet under review');

// Emergency stop for this agent across all of the principal's wallets.
await principal.setAgentBlocked(agentId, true, 'Principal emergency stop');
```

Blocking releases outstanding local-ledger reservations in the affected boundary.
Already signed EVM reservations remain held until finalized settlement or expiry;
blocking cannot recall a signature. Settled transactions remain in the append-only
wallet ledger. Unblocking permits new authorizations; it does not revive released
or expired authorizations.

## Direct authorization

The SDK also exposes the reservation operation when the caller is not using the
x402 fetch wrapper:

```ts theme={null}
const authorization = await walletAgent.authorizePayment({
  amount: '1250000',
  asset: 'USDC',
  network: 'grantex:prepaid',
  recipient: 'merchant:travel-api',
  resource: 'https://merchant.example/travel/quote',
  scope: 'travel:book',
  maxTimeoutSeconds: 120,
  idempotencyKey: crypto.randomUUID(),
});
```

The returned value is a reservation, not evidence of external-chain settlement.
For `grantex:prepaid`, the resource server must use the x402 facilitator `/v1/x402/verify` and
`/v1/x402/settle` operations. Verification is provisional: the resource server
must complete a successful settlement before starting irreversible protected
work or returning the paid result. In `sandbox_ledger`, settlement completes against
the durable Grantex ledger. Opt-in Base USDC instead uses the merchant's EVM
facilitator and finalized-chain reconciliation through the configured
`base_usdc` adapter. Other or unconfigured external providers fail closed.

## Legacy GDT utilities

`issueGDT()`, `verifyGDT()`, and `x402Middleware()` remain available for
standalone signed authorization context. They do not maintain a shared prepaid
balance, atomically reserve funds, or enforce a rolling cumulative limit across
processes by themselves. Do not present a standalone GDT's `spendLimit` claim as
proof that cumulative spend was enforced. Use the managed prepaid-wallet flow
for that guarantee.

## Failure behavior

| Condition | Result |
| - | - |
| Insufficient balance | `402 INSUFFICIENT_WALLET_FUNDS` |
| Transaction cap exceeded | `402 PER_TRANSACTION_LIMIT_EXCEEDED` |
| Rolling cap exceeded | `402 CUMULATIVE_LIMIT_EXCEEDED` |
| Layered policy deny or limit | `403 LAYERED_SPEND_POLICY_DENIED` |
| Principal approval needed | `202 PAYMENT_APPROVAL_REQUIRED` with an exact approval request |
| Recipient/scope mismatch | `403 RECIPIENT_NOT_ALLOWED` / `SCOPE_NOT_ALLOWED` |
| Scope absent from OAuth grant | `403 PAYMENT_SCOPE_NOT_GRANTED` |
| Principal block | New signatures denied; sandbox reservations released, but signed EVM holds retained until finalized reconciliation |
| Reused key with changed terms | `409 IDEMPOTENCY_CONFLICT` |
| Expired/released reservation | x402 verification fails closed |
| External provider not configured | `503 CUSTODY_ADAPTER_UNAVAILABLE` |

Verification accepts only an active reservation. After settlement, the same
authorization no longer verifies, so replay cannot execute protected work a
second time. Settlement itself is idempotent and returns its original
transaction identifier to let a resource server recover after losing the
settlement response. Side-effecting resource handlers should key their own
durable result cache or transaction on the caller's `Idempotency-Key` header.
Use the same logical identifier for that header and the SDK `idempotencyKey`
option. The default prepaid adapter does not send that option as a header. The
opt-in Base adapter also sets HTTP `Idempotency-Key` and rejects a conflicting
header. Neither can recover lost business results unless the merchant persists them.

See [x402 Architecture](/features/x402-architecture) for the trust boundaries
and [Agent Prepaid Wallets](/features/prepaid-wallets) for the complete API
lifecycle. [Agent Wallet Governance](/guides/agent-wallet-governance) assigns
responsibility across Grantex, the runtime, issuer, principal, and merchant.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.