FPSF-SS-002 — wallet-gateway Interface
Document Metadata
| Field | Value |
|---|---|
| Spec ID | FPSF-SS-002 |
| Title | Stablecoin Stack — wallet-gateway Interface |
| Version | 1.0.0 |
| Status | Draft |
| Date | 2026-03-25 |
| Author | Adalton Reis — reis@fabricpaymentstandards.org |
| Organization | Fabric Payment Standards Foundation |
| Contact | specs@fabricpaymentstandards.org> |
| License | Apache-2.0 |
| Conforms To | FPSF-SS-001 v1.0.0 |
Table of Contents
- Abstract
- Introduction
- Role Within the Stablecoin Stack
- Transport and Connection Model
- Authentication and Message Security
- Token and Domain Separator Conventions
- Wallet Initialisation
- Request–Response Operations
- Subscription Operations
- Asynchronous Status Notifications
- Message Type Reference
- Error Handling
- Security Considerations
- Conformance
- Future Work
1. Abstract
This document specifies the interface of the wallet-gateway component of the Stablecoin Stack (FPSF-SS-001). The wallet-gateway is the sole network entry point for wallet client applications. It provides a WebSocket-based messaging interface through which wallets perform balance queries, history retrieval, nonce and fee lookups, payment submissions, and acquirer registration requests. It also delivers real-time notifications on submission status, balance changes, and new confirmed transfers.
All communication is authenticated by cryptographic signature on every message. No session tokens, cookies, or API keys are used. The gateway does not execute operations directly; it mediates between the wallet client and the internal broadcast and indexing services of the Stablecoin Stack.
2. Introduction
2.1 Purpose
This specification defines the normative interface that a conformant wallet-gateway MUST implement, and that a conformant wallet client MUST be capable of consuming.
2.2 Scope
This specification covers: the transport model and connection management requirements; the authentication scheme; all request-response operations; the subscription model; the asynchronous status notification model; error categories and response formats; and security requirements specific to the gateway interface.
This specification does not cover: the internal protocol between wallet-gateway and broadcast-service; the internal protocol between wallet-gateway and balance-and-history; the on-chain Settlement Contract interface (FPSF-SS-001 Sections 11–14); or wallet key management and signing UX.
2.3 Normative References
| Reference | Description |
|---|---|
| FPSF-SS-001 v1.0.0 | Stablecoin Stack — foundational specification |
| ERC-20 | Token Standard |
| ERC-2612 | Permit Extension for EIP-20 Signed Approvals |
| EIP-712 | Typed Structured Data Hashing and Signing |
| ECDSA / FIPS 186-4 | Digital Signature Standard |
| RFC 2119 | Key words for use in RFCs to Indicate Requirement Levels |
| RFC 6455 | The WebSocket Protocol |
| RFC 8446 | The Transport Layer Security (TLS) Protocol Version 1.3 |
2.4 Conventions and Terminology
The key words MUST, MUST NOT, REQUIRED, SHALL, SHOULD, RECOMMENDED, MAY, and OPTIONAL are to be interpreted as described in RFC 2119.
All terms defined in FPSF-SS-001 Section 2.4 carry the same meaning here. Additional terms:
| Term | Definition |
|---|---|
| Wallet Client | A conformant client application connecting to the wallet-gateway on behalf of a payer. |
| Gateway Message | A JSON object transmitted over the WebSocket connection, conforming to the message envelope in Section 5.2. |
| Caller Address | The EVM address identifying the wallet in a given message. MUST match the address recovered from the message signature. |
| Domain Separator | A bytes32 EIP-712 domain separator hex string. Used as the primary token identifier throughout the gateway interface. |
| Subscription | A per-connection registration to receive real-time push notifications for specified tokens. Active only while the originating connection is open. |
| Idle Timeout | The gateway-configurable duration after which a connection with no inbound activity is closed. Recommended: 5 minutes. |
| BrokenDownAmount | A response object carrying operatorFee, acquiringFee, and totalWithFees fields. |
| Submission Status | One of the ordered states a submission passes through: ENQUEUING, PENDING, BROADCASTING, SUCCESS, FAILURE. |
| Terminal State | A submission status from which no further transitions occur: SUCCESS or FAILURE. |
3. Role Within the Stablecoin Stack
The wallet-gateway occupies the boundary between external wallet clients and the internal services of the Stablecoin Stack. It holds no token balances and cannot initiate on-chain transactions. Its responsibilities are: accepting and authenticating WebSocket connections; routing validated requests to the appropriate internal service; delivering responses and push notifications; enforcing the single-connection-per-wallet constraint; and closing idle connections with resource cleanup.
The gateway MUST NOT execute payment operations directly. Upon receiving a valid submission, it MUST enqueue the request with the broadcast-service and relay status updates to the client.
4. Transport and Connection Model
4.1 WebSocket-Only Transport
The wallet-gateway MUST expose its interface exclusively over the WebSocket protocol (RFC 6455). All WebSocket connections MUST be established over TLS (WSS). Plaintext WebSocket connections MUST be refused.
4.2 Single-Connection-Per-Wallet Constraint
A maximum of one active connection per wallet address is permitted at any time. When a new connection is established and authentication succeeds for an address that already has an active connection, the gateway MUST:
- Accept the new connection.
- Terminate the existing connection with close code
4001and reason"superseded". - Discard any pending notifications queued for the previous connection.
4.3 Connection Lifecycle and Idle Timeout
The gateway MUST enforce an idle timeout (RECOMMENDED: 5 minutes). The idle timer resets on every valid inbound message. Before closing an idle connection, the gateway SHOULD send a WebSocket ping frame. If the client responds with pong, the timer MUST reset.
Upon closing a connection for any reason, the gateway MUST immediately release all associated resources: all active subscriptions; any in-memory notification state; and the wallet address slot.
Subscription state is never persisted across connections. A reconnecting client MUST re-register all required subscriptions.
Upon establishing a new connection, the client MUST send an authenticated message within a gateway-configurable timeout (RECOMMENDED: 30 seconds). Connections that do not produce an authenticated message within this window MUST be closed.
5. Authentication and Message Security
5.1 Signature-Based Authentication
Every message sent by a wallet client MUST be individually signed by the private key corresponding to the wallet's address. The gateway MUST verify this signature on every received message, without exception.
5.2 Message Envelope
All messages sent by a wallet client MUST conform to:
interface GatewayMessage {
type: string; // Message type identifier. See Section 11.
callerAddress: string; // EVM address of the sender. 42-char hex.
deadline: number; // Unix timestamp (seconds). Reject if expired.
payload: object; // Operation-specific parameters.
signature: MessageSig;
}
interface MessageSig {
hash: string; // bytes32 — 66-char hex. EIP-712 digest of the message.
v: number; // uint8. Must be 27 or 28 (0/1 normalized automatically).
r: string; // bytes32 — 66-char hex.
s: string; // bytes32 — 66-char hex.
}
5.3 Signature Verification
The gateway MUST perform the following checks in order, rejecting immediately on any failure:
- Structure: all envelope fields present and correctly typed.
- Deadline:
deadlinestrictly greater than gateway clock. Apply configurable tolerance (RECOMMENDED: 30 seconds) for clock skew. - Signature format:
vin{0, 1, 27, 28}. Normalize 0→27, 1→28. Reject all other values.randsMUST be valid 32-byte hex strings. - Hash verification: gateway recomputes the EIP-712 digest and verifies it equals
signature.hash. - Signature recovery: recover the signing address from
(hash, v, r, s). - Address match: recovered address MUST equal
callerAddress.
The gateway MUST NOT partially process a message that fails any step.
5.4 Expiration and Replay Protection
The deadline field provides time-bounded validity. The gateway SHOULD maintain a short-lived cache of processed message hashes to detect and reject duplicates within the validity window. Primary replay protection for payment operations is enforced by usedHashes in the Settlement Contract (FPSF-SS-001 Section 19.3).
6. Token and Domain Separator Conventions
Throughout the gateway interface, supported tokens are identified primarily by their EIP-712 domain separator — a bytes32 hex string uniquely identifying a token contract within a specific network and version context. Full token metadata is available from the Basic Data Service.
The two exceptions where a token is identified by its contract address rather than its domain separator are the PayWithPermitParams and BuyAcquiringPackPermitParams structures (defined in FPSF-SS-001 Sections 8.3 and 8.4), which require the token address for on-chain permit validation.
The gateway MUST reject any request referencing a domain separator or token address not corresponding to a supported token.
7. Wallet Initialisation
When a wallet address connects for the first time — no prior state exists — the gateway MUST transparently perform:
- History collection: retrieve the complete transfer history for the address across all supported tokens.
- Balance snapshot: record current balances across all supported tokens.
This initialisation is transparent to the client — no explicit request is required. If initialisation is still in progress when a balance or history query arrives, the gateway MUST respond with an INITIALISING error (Section 12) rather than returning incomplete data.
Subsequent connections from the same address do not trigger re-initialisation.
8. Request–Response Operations
All operations use the message envelope in Section 5.2. The gateway MUST respond on the same WebSocket connection with the corresponding response type. All response messages carry a requestId field echoing the originating request's requestId.
8.1 Retrieve Nonce
Retrieves the current ERC-2612 permit nonce for a wallet address and token.
Request type: GET_NONCE
interface GetNoncePayload {
requestId: string;
domainSeparator: string; // bytes32 hex — domain separator of target token.
}
Response type: NONCE_RESULT
interface NonceResultPayload {
requestId: string;
domainSeparator: string;
nonce: string; // Current permit nonce as a decimal string (uint256).
}
The gateway MUST retrieve the nonce from live on-chain state. Stale nonces MUST NOT be returned.
8.2 Retrieve Fees
Returns the fee breakdown for a given transfer amount, token, and acquirer. Reflects the per-token fee model defined in FPSF-SS-001 Section 13.
Request type: GET_FEES
interface GetFeesPayload {
requestId: string;
domainSeparator: string; // Token for which fees are being calculated.
principal: string; // Principal amount intended for the beneficiary (decimal string).
acquirerId: string; // bytes16 hex (34-char). Pass Zero-UUID if no acquirer.
}
Response type: FEES_RESULT
interface FeesResultPayload {
requestId: string;
domainSeparator: string;
brokenDownAmount: BrokenDownAmount;
}
interface BrokenDownAmount {
operatorFee: string; // Total operator fee in token units (decimal string).
acquiringFee: string; // Acquiring fee in token units. "0" if no acquirer.
totalWithFees: string; // principal + operatorFee + acquiringFee. This is PermitParams.value.
}
The values correspond directly to calculateFees(token, principal, acquirerId) on the Settlement Contract (FPSF-SS-001 Section 13.4).
8.3 Retrieve Balance
Returns the current token balances for the wallet across one or more tokens.
Request type: GET_BALANCE
interface GetBalancePayload {
requestId: string;
domainSeparators: string[]; // One or more domain separator hex strings.
}
Response type: BALANCE_RESULT
interface BalanceResultPayload {
requestId: string;
balances: TokenBalance[];
}
interface TokenBalance {
domainSeparator: string;
balance: string; // Current balance in token units (decimal string).
}
If a domain separator is not recognized, the gateway MUST return an error for the entire request.
8.4 Retrieve Transfer History
Returns the transfer history for the wallet for one or more supported tokens. Includes only ERC-20 Transfer events for supported tokens.
Request type: GET_HISTORY
interface GetHistoryPayload {
requestId: string;
domainSeparators: string[];
cursor?: string; // Optional pagination cursor.
limit?: number; // Gateway MAY enforce a ceiling.
}
Response type: HISTORY_RESULT
interface HistoryResultPayload {
requestId: string;
transfers: TransferRecord[];
nextCursor?: string;
}
interface TransferRecord {
domainSeparator: string;
txHash: string; // 66-char hex.
blockNumber: number;
timestamp: number; // Unix timestamp of the block.
from: string; // EVM address.
to: string; // EVM address.
value: string; // Amount in token units (decimal string).
direction: "IN" | "OUT";
}
Results MUST be returned in reverse chronological order (most recent first).
8.5 Submit Payment Request
Submits a signed TransferRequest (FPSF-SS-001 Section 9.2) for processing. This is an asynchronous operation; the gateway acknowledges receipt immediately and delivers status updates via Section 10.
Request type: SUBMIT_PAYMENT
interface SubmitPaymentPayload {
requestId: string;
transferRequest: TransferRequest; // As defined in FPSF-SS-001 Section 9.2.
// Note: payWithPermitParams contains separate
// orderReference and acquirerId bytes16 fields.
}
Response type: SUBMIT_PAYMENT_ACK
interface SubmitPaymentAckPayload {
requestId: string;
payloadId: string; // Echoes transferRequest.payloadId.
status: "ENQUEUING";
}
If the submission fails envelope or first-line validation, the gateway MUST return an ERROR response rather than an acknowledgment.
8.6 Submit Acquirer Registration Request
Submits a signed BuyAcquiringPackRequest (FPSF-SS-001 Section 9.3) for processing. Asynchronous in the same manner as payment submission.
Request type: SUBMIT_ACQUIRING
interface SubmitAcquiringPayload {
requestId: string;
buyAcquiringPackRequest: BuyAcquiringPackRequest; // As defined in FPSF-SS-001 Section 9.3.
// Note: acquiringFeeBps_ is a dedicated field.
}
Response type: SUBMIT_ACQUIRING_ACK
interface SubmitAcquiringAckPayload {
requestId: string;
status: "ENQUEUING";
}
9. Subscription Operations
Subscriptions allow a connected wallet to receive real-time push notifications without polling. All subscriptions are scoped to the originating connection and cancelled when the connection closes for any reason.
9.1 Subscribe to Balance Updates
Request type: SUBSCRIBE_BALANCE
interface SubscribeBalancePayload {
requestId: string;
domainSeparators: string[];
}
Response type: SUBSCRIBE_BALANCE_ACK
interface SubscribeBalanceAckPayload {
requestId: string;
subscribedSeparators: string[];
}
Push notification type: BALANCE_UPDATE
interface BalanceUpdatePayload {
domainSeparator: string;
balance: string; // New balance in token units (decimal string).
}
9.2 Subscribe to Transfer Notifications
Request type: SUBSCRIBE_TRANSFERS
interface SubscribeTransfersPayload {
requestId: string;
domainSeparators: string[];
}
Response type: SUBSCRIBE_TRANSFERS_ACK
interface SubscribeTransfersAckPayload {
requestId: string;
subscribedSeparators: string[];
}
Push notification type: TRANSFER_NOTIFICATION
interface TransferNotificationPayload {
transfer: TransferRecord; // As defined in Section 8.4.
}
9.3 Unsubscribe
Request type: UNSUBSCRIBE
interface UnsubscribePayload {
requestId: string;
channel: "BALANCE" | "TRANSFERS";
domainSeparators: string[];
}
Response type: UNSUBSCRIBE_ACK
interface UnsubscribeAckPayload {
requestId: string;
channel: "BALANCE" | "TRANSFERS";
unsubscribedSeparators: string[];
}
10. Asynchronous Status Notifications
10.1 Submission Status Lifecycle
| Status | Terminal | Description |
|---|---|---|
ENQUEUING | No | Accepted by gateway; being handed to broadcast-service. |
PENDING | No | Received by broadcast-service; full validation in progress. |
BROADCASTING | No | Validation passed; transaction being submitted to the network. |
SUCCESS | Yes | Network accepted the transaction without revert. NOT final settlement. |
FAILURE | Yes | Submission failed. See failureCategory and failureReason. |
SUCCESS is not final settlement. Final settlement is confirmed only when transfer-history observes the PermittedTransfer event with sufficient block confirmations, delivered as a TRANSFER_NOTIFICATION.
10.2 Status Notification Message
Push notification type: SUBMISSION_STATUS
interface SubmissionStatusPayload {
payloadId: string;
submissionType: "PAYMENT" | "ACQUIRING";
status: SubmissionStatus;
failureReason?: string;
failureCategory?: FailureCategory;
txHash?: string; // Present when status is BROADCASTING or SUCCESS.
}
type SubmissionStatus = "ENQUEUING" | "PENDING" | "BROADCASTING" | "SUCCESS" | "FAILURE";
type FailureCategory = "STRUCTURAL_ERROR" | "SEMANTIC_ERROR" | "CRYPTOGRAPHIC_ERROR" | "BROADCAST_ERROR";
Failure categories correspond to those defined in FPSF-SS-001 Section 18.
11. Message Type Reference
| Type | Direction | Description |
|---|---|---|
GET_NONCE | Client → Gateway | Request current permit nonce. |
NONCE_RESULT | Gateway → Client | Current nonce. |
GET_FEES | Client → Gateway | Request fee breakdown (per-token, with separate acquirerId). |
FEES_RESULT | Gateway → Client | BrokenDownAmount with operatorFee, acquiringFee, totalWithFees. |
GET_BALANCE | Client → Gateway | Request balances for one or more tokens. |
BALANCE_RESULT | Gateway → Client | Balance array. |
GET_HISTORY | Client → Gateway | Request transfer history. |
HISTORY_RESULT | Gateway → Client | Transfer records. |
SUBMIT_PAYMENT | Client → Gateway | Submit a TransferRequest payload. |
SUBMIT_PAYMENT_ACK | Gateway → Client | Acknowledgment with initial ENQUEUING status. |
SUBMIT_ACQUIRING | Client → Gateway | Submit a BuyAcquiringPackRequest payload. |
SUBMIT_ACQUIRING_ACK | Gateway → Client | Acknowledgment with initial ENQUEUING status. |
SUBSCRIBE_BALANCE | Client → Gateway | Subscribe to balance update notifications. |
SUBSCRIBE_BALANCE_ACK | Gateway → Client | Subscription confirmation. |
SUBSCRIBE_TRANSFERS | Client → Gateway | Subscribe to transfer notifications. |
SUBSCRIBE_TRANSFERS_ACK | Gateway → Client | Subscription confirmation. |
UNSUBSCRIBE | Client → Gateway | Cancel a subscription. |
UNSUBSCRIBE_ACK | Gateway → Client | Cancellation confirmation. |
BALANCE_UPDATE | Gateway → Client | Push: balance changed on a subscribed token. |
TRANSFER_NOTIFICATION | Gateway → Client | Push: new confirmed transfer on a subscribed token. |
SUBMISSION_STATUS | Gateway → Client | Push: submission status transition. |
ERROR | Gateway → Client | Error response for any failed request. |
12. Error Handling
Error message type: ERROR
interface ErrorPayload {
requestId?: string;
errorCode: string;
errorCategory: ErrorCategory;
message: string; // Human-readable. MUST NOT include internal implementation detail.
}
type ErrorCategory =
| "STRUCTURAL_ERROR"
| "AUTHENTICATION_ERROR"
| "SEMANTIC_ERROR"
| "NOT_FOUND"
| "RATE_LIMIT"
| "INTERNAL_ERROR";
| Error Code | Category | Description |
|---|---|---|
MISSING_FIELD | STRUCTURAL_ERROR | A required envelope or payload field is absent. |
INVALID_FORMAT | STRUCTURAL_ERROR | A field does not conform to its expected format. |
INVALID_SIGNATURE | AUTHENTICATION_ERROR | Signature malformed or cannot be verified. |
ADDRESS_MISMATCH | AUTHENTICATION_ERROR | Recovered signer does not match callerAddress. |
EXPIRED_DEADLINE | AUTHENTICATION_ERROR | Message deadline is in the past. |
UNSUPPORTED_TOKEN | SEMANTIC_ERROR | Domain separator or token address not recognized. |
NONCE_MISMATCH | SEMANTIC_ERROR | Supplied nonce does not match on-chain state. |
UNKNOWN_PAYLOAD_ID | SEMANTIC_ERROR | payloadId does not correspond to a known session. |
ALREADY_SUBMITTED | SEMANTIC_ERROR | payloadId has already been processed. |
WALLET_NOT_FOUND | NOT_FOUND | No state found for the given wallet address. |
INITIALISING | SEMANTIC_ERROR | Wallet state still being collected. Retry shortly. |
RATE_LIMIT_EXCEEDED | RATE_LIMIT | Request rate exceeded. |
INTERNAL_ERROR | INTERNAL_ERROR | Unexpected gateway-side failure. |
The gateway MUST NOT include stack traces, internal service identifiers, or implementation-sensitive detail in error responses.
13. Security Considerations
Per-message signature verification. The gateway MUST verify the message signature on every received message without exception.
Single-connection enforcement. Prevents a stale or compromised session from continuing to receive notifications after a new authorized connection supersedes it.
Idle timeout and resource cleanup. Bounds the period during which server-side resources, including subscription registrations, are held for a non-communicating client.
No key material exposure. Wallet clients MUST NOT transmit private keys, seed phrases, or any key derivation material to the gateway. All signing MUST be performed locally.
TLS requirement. All connections MUST use TLS. The gateway MUST refuse plaintext connections.
Rate limiting. The gateway SHOULD implement per-connection and per-address rate limiting. Rate-limit errors MUST use the RATE_LIMIT error category.
No fund custody. The gateway holds no token balances. A gateway compromise does not directly result in loss of funds — cryptographic integrity is enforced by the Settlement Contract independently.
14. Conformance
14.1 Conformant wallet-gateway
A conformant wallet-gateway MUST:
- Expose its interface exclusively over WebSocket with TLS (Section 4.1).
- Enforce the single-connection-per-wallet constraint with most-recent-connection precedence (Section 4.2).
- Enforce an idle timeout and release all connection resources on close (Section 4.3).
- Verify the message signature on every received message, rejecting on any failure in the sequence of Section 5.3.
- Perform transparent wallet initialisation on first connection and respond with
INITIALISINGif a query arrives before completion (Section 7). - Implement all request-response operations defined in Section 8, including the updated
GET_FEESresponse structure with separateoperatorFee,acquiringFee, andtotalWithFeesfields. - Accept
SUBMIT_PAYMENTpayloads containingPayWithPermitParamswith separateorderReferenceandacquirerIdbytes16fields, and forward them correctly to the broadcast-service. - Accept
SUBMIT_ACQUIRINGpayloads containingBuyAcquiringPackPermitParamswith theacquiringFeeBps_field. - Implement the subscription model of Section 9 with subscriptions scoped to the originating connection.
- Deliver
SUBMISSION_STATUSnotifications through to a terminal state for all accepted submissions (Section 10). - Return structured
ERRORmessages conforming to Section 12 for all failures.
14.2 Conformant wallet client
A conformant wallet client MUST:
- Connect exclusively via WSS.
- Include a valid signature in every message sent to the gateway.
- Handle unexpected disconnection gracefully and re-establish the connection when required.
- Re-register all required subscriptions after reconnection.
- Construct
PayWithPermitParamswithorderReferenceandacquirerIdas separatebytes16fields — not concatenated. - Call
GET_FEESwith the correctacquirerId(or Zero-UUID) and use thetotalWithFeesvalue asPermitParams.value. - Not treat a
SUCCESSsubmission status as final settlement. - Not transmit key material of any kind to the gateway.
15. Future Work
Per-token fee model display. The GET_FEES response already returns operatorFee and acquiringFee separately. A future MINOR version may add a feeBreakdown field with explicit baseFee and basisPointsFee sub-components for UI display purposes.
Person-to-person transfers. The architecture supports P2P token transfers. A future wallet specification (FPSF-SS-005) will define the client-side interface for this use case.
Wallet certification. FPSF-SS-005 (planned) will define a conformance test suite and certification programme.
FPSF-SS-002 v1.0.0 · Draft · Fabric Payment Standards Foundation · Apache-2.0