Vouchers
Vouchers are prepaid, multi-use payment tokens that consumers create from their wallet. They reserve a specific amount of tokens and produce an encrypted token string (spay_...) that can be presented to providers as proof of payment.
How Vouchers Work
Consumer Provider
┌────────────────┐ ┌────────────────┐
│ 1. Create │ │ │
│ voucher │ │ │
│ (reserve │ │ │
│ tokens) │ │ │
│ │ │ │
│ 2. Get token │──── spay_ ──▶│ 3. Verify │
│ (spay_...) │ │ token │
│ │ │ │
│ │ │ 4. Execute │
│ │ │ service │
│ │ │ │
│ │◀─────────────│ 5. Settle │
│ 6. Tokens │ │ (charge) │
│ deducted │ │ │
└────────────────┘ └────────────────┘
Consumer Side
- Consumer creates a voucher in their wallet, specifying an amount to reserve
- The system checks the wallet has sufficient available balance
- The reserved amount is locked in the wallet (
lockedAmountincreases) - An encrypted token (
spay_...) is issued
Provider Side
- Provider receives the
spay_...token from the consumer - Provider SDK verifies the token and locks tokens within the voucher for the request
- Provider executes the business logic
- Provider settles with the actual amount consumed
- Unused reservation returns to the voucher balance for future use
Creating Vouchers
Consumers create vouchers through the wallet UI. Each voucher has:
| Field | Description |
|---|---|
| Name | A label for the voucher (e.g., "API access for Agent X") |
| Amount | Tokens to reserve from the wallet |
| Expiry | Optional expiration (0–120 days) |
| Spend Limit | Optional per-period and per-request caps |
Amount Reservation
When a voucher is created:
- The wallet's
lockedAmountincreases by the voucher amount - These tokens are guaranteed available until the voucher expires or is removed
- The locked tokens cannot be used by other vouchers or
uptolocks
Expiry
If an expiry is set, a background job automatically revokes the voucher when it expires and releases the remaining reserved tokens back to the wallet.
Spend Limits
Vouchers support optional rate limiting on top of the hard amount:
| Limit | Description |
|---|---|
| Period limit | Max tokens per hour/day/month |
| Per-request limit | Max tokens per single request |
These are checked during verifyVoucherPayment in addition to the remaining balance.
Multi-Use Vouchers
Vouchers are multi-use — they can be used for multiple requests until the reserved balance runs out. Each request creates a separate TokenLock scoped to the voucher.
Example: A voucher with 10,000 tokens reserved can serve:
- 100 requests at 100 tokens each
- 20 requests at 500 tokens each
- Any mix until the balance is exhausted
Voucher Token Format
The token string (spay_...) is an encrypted payload containing:
- Account reference
- Voucher ID
- Issuance timestamp
The token is encrypted with AES and can only be decrypted by the SolvaPay backend. It cannot be forged or tampered with.
Token Revocation
Reissuing a voucher token invalidates all previously issued tokens for that voucher. The backend checks the issuance timestamp against the stored tokenIssuedAt to reject stale tokens.
Payment Flow (Two-Phase)
The voucher payment flow mirrors the upto scheme but locks tokens within the voucher's reserved balance rather than from the wallet directly.
Verify
POST /sdk/vouchers/verify
{
"token": "spay_abc123...",
"maxAmount": 500,
"productRef": "prd_myapi",
"providerId": "prv_xxx"
}
- Decrypts and validates the voucher token
- Checks voucher is active and not expired
- Checks
remaining >= maxAmount - Atomically increments
voucher.spentbymaxAmount - Creates a
TokenLockwithvoucherIdset - Returns lock ID, account identity, and remaining balance
Settle
POST /sdk/vouchers/settle
{
"lockId": "...",
"amount": 350,
"description": "Analysis completed"
}
- Validates the lock is
reservedand voucher-backed - If
amount < reservedAmount, refunds the difference tovoucher.spent - Decrements
Account.lockedAmountby the settled amount (tokens leave the wallet permanently) - Records
captureandreleaseledger entries - Credits the provider (minus platform fee)
- Marks the lock as
settled
Release
POST /sdk/vouchers/release
{
"lockId": "...",
"reason": "cancelled"
}
- Decrements
voucher.spentby the full reserved amount - Marks the lock as
released - Tokens remain locked by the voucher but available for future requests
Idempotency
- Each
verifyVoucherPaymentcreates a uniqueTokenLockwith a reference ID — this is the idempotency boundary settleVoucherPaymentcheckslock.status === 'reserved'— a second settle on the same lock fails cleanly with a409 Conflict- Ledger entries use idempotency keys derived from
lockId + actionto prevent duplicates
Voucher States
active ──▶ paused ──▶ active (can be toggled)
active ──▶ revoked (manual removal or expiry)
| State | Description |
|---|---|
active | Voucher is usable, tokens are reserved |
paused | Voucher is temporarily disabled, reserved tokens are released |
revoked | Voucher is permanently disabled, remaining tokens are released |
When a voucher is paused, the remaining balance is released from lockedAmount. When resumed, the balance is re-locked (if sufficient funds are available).
Removing a Voucher
When a voucher is removed:
- Any remaining balance (
amount - spent) is released fromlockedAmount - The voucher is removed from the account
- Previously issued tokens become invalid
SDK Usage
With payable() Wrapper
The simplest way — the SDK handles everything:
const payable = solvaPay.payable({
scheme: 'voucher',
product: 'prd_myapi',
providerId: 'prv_xxx',
maxPrice: 500,
})
server.tool('analyze', payable.mcp(async (args) => {
// args._voucherId — the voucher ID
// args._accountRef — consumer account
// args._identity — { fingerprint, publicKey }
// args._remaining — voucher balance after reservation
const result = await runAnalysis(args)
await args.settle(result.tokenCost)
return result
}))
Direct API Client
For full control:
// Verify
const lock = await solvaPay.apiClient.verifyVoucherPayment({
token: 'spay_abc123...',
maxAmount: 500,
productRef: 'prd_myapi',
providerId: 'prv_xxx',
})
// Execute business logic...
// Settle
const result = await solvaPay.apiClient.settleVoucherPayment({
lockId: lock.lockId,
amount: 350,
})
// Or release
await solvaPay.apiClient.releaseVoucherPayment({
lockId: lock.lockId,
reason: 'operation cancelled',
})
Read-Only Inspection
Check a voucher without locking tokens:
const info = await solvaPay.apiClient.resolveVoucher({
token: 'spay_abc123...',
})
console.log(info.balance) // remaining tokens
console.log(info.status) // 'active'
console.log(info.spendLimit) // rate limits, if set
Next Steps
- SDK Integration — Full reference for all payment schemes
- Provider Integration — Accept voucher payments and track earnings
- Security & Compliance — Audit trails and fraud protection