Skip to main content

Solana Pay Specification

Summary

A standard protocol to encode Solana transaction requests within URLs to enable payments and other use cases.

Rough consensus on this spec has been reached, and implementations exist in Phantom, FTX, and Slope.

This standard draws inspiration from BIP 21 and EIP 681.

Motivation

A standard URL protocol for requesting native SOL transfers, SPL Token transfers, and Solana transactions allows for a better user experience across apps and wallets in the Solana ecosystem.

These URLs may be encoded in QR codes or NFC tags, or sent between users and applications to request payment and compose transactions.

Applications should ensure that a transaction has been confirmed and is valid before they release goods or services being sold, or grant access to objects or events.

Mobile wallets should register to handle the URL scheme to provide a seamless yet secure experience when Solana Pay URLs are encountered in the environment.

By standardizing a simple approach to solving these problems, we ensure basic compatibility of applications and wallets so developers can focus on higher level abstractions.

Specification: Transfer Request

A Solana Pay transfer request URL describes a non-interactive request for a SOL or SPL Token transfer.

solana:<recipient>
?amount=<amount>
&spl-token=<spl-token>
&reference=<reference>
&label=<label>
&message=<message>
&memo=<memo>

The request is non-interactive because the parameters in the URL are used by a wallet to directly compose a transaction.

Recipient

A single recipient field is required as the pathname. The value must be the base58-encoded public key of a native SOL account. Associated token accounts must not be used.

Instead, to request an SPL Token transfer, the spl-token field must be used to specify an SPL Token mint, from which the associated token address of the recipient must be derived.

Amount

A single amount field is allowed as an optional query parameter. The value must be a non-negative integer or decimal number of "user" units. For SOL, that's SOL and not lamports. For tokens, use uiAmountString and not amount.

0 is a valid value. If the value is a decimal number less than 1, it must have a leading 0 before the .. Scientific notation is prohibited.

If a value is not provided, the wallet must prompt the user for the amount. If the number of decimal places exceed what's supported for SOL (9) or the SPL Token (mint specific), the wallet must reject the URL as malformed.

SPL Token

A single spl-token field is allowed as an optional query parameter. The value must be the base58-encoded public key of an SPL Token mint account.

If the field is not provided, the URL describes a native SOL transfer. If the field is provided, the Associated Token Account convention must be used.

The wallet must derive the ATA address from the recipient and spl-token fields. Transfers to auxiliary token accounts are not supported.

Reference

Multiple reference fields are allowed as optional query parameters. The values must be base58-encoded public keys.

If the values are provided, the wallet must include them in the order provided as read-only, non-signer keys to the SystemProgram.transfer or TokenProgram.transfer/TokenProgram.transferChecked instruction in the payment transaction. The values may or may not be unique to the payment request, and may or may not correspond to an account on Solana.

Because Solana validators index transactions by these account keys, reference values can be used as client IDs (IDs usable before knowing the eventual payment transaction). The getSignaturesForAddress RPC method can be used locate transactions this way.

Label

A single label field is allowed as an optional query parameter. The value must be a URL-encoded UTF-8 string that describes the source of the transfer request.

For example, this might be the name of a brand, store, application, or person making the request. The wallet should URL-decode the value and display the decoded value to the user.

Message

A single message field is allowed as an optional query parameter. The value must be a URL-encoded UTF-8 string that describes the nature of the transfer request.

For example, this might be the name of an item being purchased, an order ID, or a thank you note. The wallet should URL-decode the value and display the decoded value to the user.

Memo

A single memo field is allowed as an optional query parameter. The value must be a URL-encoded UTF-8 string that must be included in an SPL Memo instruction in the payment transaction.

The wallet must URL-decode the value and should display the decoded value to the user. The memo will be recorded by validators and should not include private or sensitive information.

A single SPL Memo instruction must be included immediately before the SOL or SPL Token transfer instruction to avoid ambiguity with other instructions in the transaction.

Examples

URL describing a transfer request for 1 SOL.
solana:mvines9iiHiQTysrwkJjGf2gb9Ex9jXJX8ns3qwf2kN?amount=1&label=Michael&message=Thanks%20for%20all%20the%20fish&memo=OrderId12345
URL describing a transfer request for 0.01 USDC.
solana:mvines9iiHiQTysrwkJjGf2gb9Ex9jXJX8ns3qwf2kN?amount=0.01&spl-token=EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v
URL describing a transfer request for SOL. The user must be prompted for the amount.
solana:mvines9iiHiQTysrwkJjGf2gb9Ex9jXJX8ns3qwf2kN&label=Michael

Specification: Transaction Request

A Solana Pay transaction request URL describes an interactive request for any Solana transaction.

solana:<link>

The request is interactive because the parameters in the URL are used by a wallet to make an HTTP request to compose a transaction.

A single link field is required as the pathname. The value must be a conditionally URL-encoded absolute HTTPS URL.

If the URL contains query parameters, it must be URL-encoded. Protocol query parameters may be added to this specification. URL-encoding the value prevents conflicting with protocol parameters.

If the URL does not contain query parameters, it should not be URL-encoded. This produces a shorter URL and a less dense QR code.

In either case, the wallet must URL-decode the value. This has no effect if the value isn't URL-encoded. If the decoded value is not an absolute HTTPS URL, the wallet must reject it as malformed.

GET Request

The wallet should make an HTTP GET JSON request to the URL. The request should not identify the wallet or the user.

The wallet should make the request with an Accept-Encoding header, and the application should respond with a Content-Encoding header for HTTP compression.

The wallet should display the domain of the URL as the request is being made.

GET Response

The wallet must handle HTTP client error, server error, and redirect responses. The application must respond with these, or with an HTTP OK JSON response with a body of

{"label":"<label>","icon":"<icon>"}

The <label> value must be a UTF-8 string that describes the source of the transaction request. For example, this might be the name of a brand, store, application, or person making the request.

The <icon> value must be an absolute HTTP or HTTPS URL of an icon image. The file must be an SVG, PNG, or WebP image, or the wallet must reject it as malformed.

The wallet should not cache the response except as instructed by HTTP caching response headers.

The wallet should display the label and render the icon image to user.

POST Request

The wallet must make an HTTP POST JSON request to the URL with a body of

{"account":"<account>"}

The <account> value must be the base58-encoded public key of an account that may sign the transaction.

The wallet should make the request with an Accept-Encoding header, and the application should respond with a Content-Encoding header for HTTP compression.

The wallet should display the domain of the URL as the request is being made. If a GET request was made, the wallet should also display the label and render the icon image from the response.

POST Response

The wallet must handle HTTP client error, server error, and redirect responses. The application must respond with these, or with an HTTP OK JSON response with a body of

{"transaction":"<transaction>"}

The <transaction> value must be a base64-encoded serialized transaction. The wallet must base64-decode the transaction and deserialize it.

The application may respond with a partially or fully signed transaction. The wallet must validate the transaction as untrusted.

If the transaction signatures are empty:

  • The application should set the feePayer to the account in the request, or the zero value (new PublicKey(0) or new PublicKey("11111111111111111111111111111111")).
  • The application should set the recentBlockhash to the latest blockhash, or the zero value (new PublicKey(0).toBase58() or "11111111111111111111111111111111").
  • The wallet must ignore the feePayer in the transaction and set the feePayer to the account in the request.
  • The wallet must ignore the recentBlockhash in the transaction and set the recentBlockhash to the latest blockhash.

If the transaction signatures are nonempty:

The wallet must only sign the transaction with the account in the request, and must do so only if a signature for the account in the request is expected.

If any signature except a signature for the account in the request is expected, the wallet must reject the transaction as malicious.

The application may also include an optional message field in the response body:

{"message":"<message>","transaction":"<transaction>"}

The <message> value must be a UTF-8 string that describes the nature of the transaction response.

For example, this might be the name of an item being purchased, a discount applied to the purchase, or a thank you note. The wallet should display the value to the user.

The wallet and application should allow additional fields in the request body and response body, which may be added by future specification.

Example

URL describing a transaction request.
solana:https://example.com/solana-pay
URL describing a transaction request with query parameters.
solana:https%3A%2F%2Fexample.com%2Fsolana-pay%3Forder%3D12345
GET Request
GET /solana-pay?order=12345 HTTP/1.1
Host: example.com
Connection: close
Accept: application/json
Accept-Encoding: br, gzip, deflate
GET Response
HTTP/1.1 200 OK
Connection: close
Content-Type: application/json
Content-Length: 62
Content-Encoding: gzip

{"label":"Michael Vines","icon":"https://example.com/icon.svg"}
POST Request
POST /solana-pay?order=12345 HTTP/1.1
Host: example.com
Connection: close
Accept: application/json
Accept-Encoding: br, gzip, deflate
Content-Type: application/json
Content-Length: 57

{"account":"mvines9iiHiQTysrwkJjGf2gb9Ex9jXJX8ns3qwf2kN"}
POST Response
HTTP/1.1 200 OK
Connection: close
Content-Type: application/json
Content-Length: 298
Content-Encoding: gzip

{"message":"Thanks for all the fish","transaction":"AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAECC4JMKqNplIXybGb/GhK1ofdVWeuEjXnQor7gi0Y2hMcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQECAAAMAgAAAAAAAAAAAAAA"}

Extensions

Additional formats and fields may be incorporated into this specification to enable new use cases while ensuring compatibility with apps and wallets.

Please open a Github issue to propose changes to the specification in order to solicit feedback from application and wallet developers.

An actual example of such a proposal.