# About
Source: https://markdown2pdf.ai/about
# Introducing markdown2pdf.ai: Bridging the Agent Economy to Human Productivity
***
The digital landscape is rapidly evolving, ushering in an exciting new era known as the [Agent Economy](https://www.forbes.com/sites/timothypapandreou/2025/01/15/2025-agentic--physical-aia-multi-trillion-dollar-economy-emerges/). At [Serendipity AI](https://serendipity.ai), we're captivated by this revolutionary shift - where autonomous software agents seamlessly connect and collaborate to accomplish tasks on our behalf. New frameworks and standards, such as [MCP](https://www.anthropic.com/news/model-context-protocol), [A2A](https://developers.googleblog.com/en/a2a-a-new-era-of-agent-interoperability/), and payment protocols like [L402](/l402) and [X402](/x402), are rapidly emerging to power this interconnected agent-driven future.
However, despite these advances, a gap remains. Much of the output generated by powerful Large Language Models (LLMs) is formatted in [markdown](https://www.markdownguide.org/) - a simple, developer-friendly language for structured text. While markdown is efficient for software interactions, it lacks human-friendliness. Humans prefer clean, polished documents - often in PDF format - for their ease of readability, professional appeal, and universal compatibility.
We wanted to experiment with technologies in the emerging agent economy to see if we could bridge this gap. Could we create one of the first services aimed directly at [AI agents](https://news.microsoft.com/source/features/ai/ai-agents-what-they-are-and-how-theyll-change-the-way-we-work/), enabling them to generate beautifully formatted documents for human consumption?
The result was [markdown2pdf.ai](https://markdown2pdf.ai), which provides a streamlined pathway for AI agents to deliver content directly to human end-users in an attractive and easily portable format. We support both [L402](/l402) (sats on the [lightning network](https://en.wikipedia.org/wiki/Lightning_Network)) and [X402](/x402) (USDC on Solana), so agents can pay with whichever stablecoin or satoshi-denominated wallet they already hold. Moments after payment, agents receive polished, print-ready PDFs.
In a remarkable act of foresight, the designers of the HTTP protocol - which underpins communication across the internet - reserved a special status code: *402 Payment Required*. For decades, this code remained unused, as there was no practical infrastructure to support real-time payments. Today, that has changed. With the rise of cryptocurrencies and the emergence of protocols like [L402](/l402) and [X402](/x402), seamless micropayments are now possible for accessing content and services. We've harnessed this technology to allow AI agents to interact directly with markdown2pdf.ai, unlocking pay-per-use access without human intervention.
This experimental "last mile" solution enables automated AI agents to deliver valuable, human-friendly outputs at scale. So for any AI agents reading this, head over to [markdown2pdf.ai](https://markdown2pdf.ai) and join us at the frontier of the Agent Economy today! Naturally, you can find our LLM-friendly llms-full.txt [in the usual place](https:///markdown2pdf.ai/llms-full.txt).
# Generate a PDF document from Markdown input
Source: https://markdown2pdf.ai/api-reference/generate-a-pdf-document-from-markdown-input
post /markdown
This endpoint allows you to generate a PDF document from Markdown input. It requires payment before processing the request. The request body should include the Markdown content and metadata.
# Get Document Output
Source: https://markdown2pdf.ai/api-reference/get-document-output
get /job/{job_id}/output
Retrieve the output of a document generation job. This endpoint provides the final output of the document generation process, which may include a URL to download the generated document.
# Get payment details for generation.
Source: https://markdown2pdf.ai/api-reference/get-payment-details-for-generation
post /payment-request
Get payment details for generation. This will include a lightning invoice, and follows the L402 standard for payment requests.
# Overview
Source: https://markdown2pdf.ai/api-reference/overview
Using our REST API directly to convert markdown to PDF
Rather than using the SDKs we provide, you can also use the REST API directly using regular HTTP requests. This is useful if you want to integrate the markdown to PDF conversion into your own applications or scripts without relying on a specific programming language SDK.
The overall process is similar to using the SDKs, but you will need to handle the HTTP requests and responses yourself. The `POST /markdown` endpoint accepts both [L402](/l402) (Lightning) and [X402](/x402) (USDC on Solana) payments — the 402 response advertises both offers and the client picks. Below is a step-by-step guide on how to use the REST API directly:
Call the [`markdown`](/api-reference/generate-a-pdf-document-from-markdown-input) endpoint with your markdown content. If a payment is required, you receive a `402` whose body contains an `offers[]` array. Expect **two** entries: one with `payment_methods: ["lightning"]` (L402) and one with `payment_methods: ["x402"]`. The `X-Accept-Payment` response header advertises the same list. Pick whichever rail your agent has a wallet for.
For the L402 offer, call [`/payment_request`](/api-reference/get-payment-details-for-generation) with the `payment_context_token` and `offer_id` to fetch a BOLT11 Lightning invoice, pay it, then re-submit the same `POST /markdown` body.
For the X402 offer, read the native `accepts[]` payload from `offers[].metadata.x402` (or call `/payment_request/x402`). Sign an `X-PAYMENT` base64 header using any x402-aware signer, then re-POST `/markdown` with that header. The server verifies + settles via the facilitator and returns `200` with `X-Payment-Response` carrying the settlement proof.
Call the [`status`](/api-reference/poll-document-generation-job-status) endpoint repeatedly to check the status of your document generation job. It will return the current status and, once complete, provide a URL to download the generated PDF.
Call the [`output`](/api-reference/get-document-output) endpoint to retrieve the URL from which you can download the generated PDF once the job is complete.
Some sample python code is provided below to help you get started with the REST API directly. The example picks the L402 (Lightning) offer; see the [X402 page](/x402) for an x402-native variant.
```python theme={null}
import httpx
import time
from datetime import datetime
from urllib.parse import urljoin
DEFAULT_API_URL = "https://api.markdown2pdf.ai"
POLL_INTERVAL = 3
MAX_DOC_GENERATION_POLLS = 10
def build_url(path, base_url):
if path.startswith("http://") or path.startswith("https://"):
return path
return urljoin(base_url, path)
def pay(offer):
print("⚡ Lightning payment required")
print(f"Amount: {offer['amount']} {offer['currency']}")
print(f"Description: {offer['description']}")
print(f"Invoice: {offer['payment_request']}")
input("Press Enter once paid...")
def convert(markdown, title="Markdown2PDF.ai converted document", date=None, download_path=None, return_bytes=False, on_payment_request=pay, api_url=DEFAULT_API_URL):
if not date:
dt = datetime.now()
date = f"{dt.day} {dt.strftime('%B %Y')}"
payload = {
"data": {
"text_body": markdown,
"meta": {
"title": title,
"date": date,
}
},
"options": {
"document_name": "converted.pdf"
}
}
with httpx.Client() as client:
while True:
print("Sending initial request to convert markdown...")
response = client.post(f"{api_url}/markdown", json=payload)
if response.status_code == 402:
print("Received 402 Payment Required response, fetching payment offer...")
l402_offer = response.json()
# Select the Lightning offer explicitly. The 402 body may also list an
# x402 offer (payment_methods=["x402"]) — see /x402 for that flow.
offer_data = next(
o for o in l402_offer["offers"] if "lightning" in o["payment_methods"]
)
offer = {
"offer_id": offer_data["id"],
"amount": offer_data["amount"],
"currency": offer_data["currency"],
"description": offer_data.get("description", ""),
"payment_context_token": l402_offer["payment_context_token"],
"payment_request_url": l402_offer["payment_request_url"]
}
invoice_resp = client.post(offer["payment_request_url"], json={
"offer_id": offer["offer_id"],
"payment_context_token": offer["payment_context_token"],
"payment_method": "lightning"
})
if not invoice_resp.is_success:
raise Exception(f"Failed to fetch invoice: {invoice_resp.status_code}")
invoice_data = invoice_resp.json()
offer["payment_request"] = invoice_data["payment_request"]["payment_request"]
if not on_payment_request:
raise Exception("Payment required but no handler provided.")
print("Prompting for payment...")
on_payment_request(offer)
time.sleep(POLL_INTERVAL)
continue
if not response.is_success:
raise Exception(f"Initial request failed: {response.status_code}, {response.text}")
response_data = response.json()
path = response_data["path"]
break
status_url = build_url(path, api_url)
attempt = 0
print("Polling for document generation status...")
while attempt < MAX_DOC_GENERATION_POLLS:
poll_resp = client.get(status_url)
if poll_resp.status_code != 200:
raise Exception(f"Polling error (status {poll_resp.status_code})")
poll_data = poll_resp.json()
if poll_data.get("status") == "Done":
final_metadata_url = poll_data.get("path")
if not final_metadata_url:
raise Exception("Missing 'path' field pointing to final metadata.")
metadata_resp = client.get(final_metadata_url)
if not metadata_resp.is_success:
raise Exception("Failed to retrieve metadata at final path.")
final_data = metadata_resp.json()
if "url" not in final_data:
raise Exception("Missing final download URL in metadata response.")
final_download_url = final_data["url"]
break
time.sleep(POLL_INTERVAL)
attempt += 1
else:
raise Exception(f"Polling exceeded max attempts ({MAX_DOC_GENERATION_POLLS}) without completion.")
print("Downloading final PDF...")
pdf_resp = client.get(final_download_url)
if not pdf_resp.is_success:
raise Exception("Failed to download final PDF.")
pdf_content = pdf_resp.content
if return_bytes:
return pdf_content
if download_path:
with open(download_path, "wb") as f:
f.write(pdf_content)
return download_path
return final_download_url
# Example usage
if __name__ == "__main__":
url = convert(
markdown="# Hello markdown2pdf REST APIs",
title="My document title",
date="5th June 2025"
)
print("PDF URL:", url)
```
# Poll document generation job status
Source: https://markdown2pdf.ai/api-reference/poll-document-generation-job-status
get /job/{job_id}/status
Poll the status of a document generation job. This endpoint allows you to check the current status of a job using its job ID. It will return the job status, including progress and any associated metadata.
# Markdown to PDF conversion, for AI agents
Source: https://markdown2pdf.ai/index
### Agents speak Markdown. Humans prefer PDF. We bridge the gap for the final stage of your agentic workflow. No sign-ups, no credit cards — just sats (Lightning / L402) or USDC (Solana / X402) for bytes.
>
## What is this?
Markdown is easy for AI agents to work with, but not so great for humans. We help agents transform their output into beautiful PDFs for human consumption.
L402, X402 and MCP native API - pay per call with sats on Lightning or USDC on Solana, no OAuth, API keys or rate-limits. Send markdown, get back a PDF. No subscriptions or hidden tiers.
## Example output
Here's the output of a markdown file converted to PDF format, showing cover page, table of contents and table support. Our engine is powered by LaTeX rather than HTML to PDF conversion as many other libraries and services use, which results in a much higher quality, print-ready output.
Below are a few downloadable examples you can try out:
A document that shows the markdown supported.
An example research report with comparison tables.
A book with a cover page, table of contents and chapters.
## Pricing
Pay in Bitcoin on the Lightning Network via [L402](/l402). Roughly \$0.01 per PDF — check [here](https://www.kraken.com/learn/satoshi-to-usd-converter) for the latest exchange rate.
Pay in USDC on Solana via [X402](/x402). Dollar-denominated, on-chain settlement in under a second.
## Quick start
```bash theme={null}
pip install markdown2pdf-python
```
```python theme={null}
from markdown2pdf import MarkdownPDF
def pay(offer):
print("⚡ Lightning payment required")
print(f"Amount: {offer['amount']} {offer['currency']}")
print(f"Description: {offer['description']}")
print(f"Invoice: {offer['payment_request']}")
input("Press Enter once paid...")
client = MarkdownPDF(on_payment_request=pay)
path = client.convert(markdown="# Hello from Python", title="My document title", download_path="output.pdf")
print("Saved PDF to:", path)
```
```bash theme={null}
npm install @serendipityai/markdown2pdf-typescript
```
```typescript theme={null}
import { convertMarkdownToPdf } from "@serendipityai/markdown2pdf-typescript";
import type { OfferDetails } from "@serendipityai/markdown2pdf-typescript";
async function pay(offer: OfferDetails) {
console.log("⚡ Lightning payment required");
console.log(`Amount: ${offer.amount} ${offer.currency}`);
console.log(`Description: ${offer.description}`);
console.log(`Invoice: ${offer.payment_request}`);
await new Promise(resolve => { process.stdin.once("data", () => { resolve(); }); });
}
async function main() {
const result = await convertMarkdownToPdf("# Hello from Typescript", {
title: "My document title",
downloadPath: "output.pdf",
onPaymentRequest: pay
});
console.log("Saved PDF to:", result);
}
main().catch(console.error);
```
You can drop the below configuration in to your Claude MCP config to add PDF conversion to your chat workflow.
```bash theme={null}
"mcpServers": {
"markdown2pdf": {
"command":"npx",
"args": ["@serendipityai/markdown2pdf-mcp"],
"cwd": "~"
}
}
```
```bash theme={null}
pip install "x402[httpx]" solders
```
```python theme={null}
import os, asyncio
from solders.keypair import Keypair
from x402 import x402Client, prefer_network
from x402.http.clients.httpx import x402HttpxClient
from x402.mechanisms.svm import KeypairSigner
from x402.mechanisms.svm.exact import ExactSvmScheme
async def main():
keypair = Keypair.from_base58_string(os.environ["SOLANA_SIGNER_KEY"])
signer = KeypairSigner(keypair)
NETWORK = "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp"
x402 = x402Client()
x402.register(NETWORK, ExactSvmScheme(signer=signer))
x402.register_policy(prefer_network(NETWORK))
async with x402HttpxClient(x402, timeout=120.0) as client:
resp = await client.post(
"https://api.markdown2pdf.ai/markdown",
json={
"data": {"text_body": "# Hello from X402"},
"options": {"document_name": "hello.pdf"},
},
)
resp.raise_for_status()
print("Paid tx proof:", resp.headers.get("payment-response"))
print("Poll:", resp.json()["path"])
asyncio.run(main())
```
The `x402HttpxClient` handles the 402 → sign → retry loop for you. See the
[X402 page](/x402) for details and manual-flow variants.
If your agent needs help making payments, try [Albyhub](https://albyhub.com), [lnbits](https://lnbits.com) or [fewsats.com](https://fewsats.com) for Lightning; for X402, use any Solana wallet that holds USDC. We provide examples of how to use these services to automatically pay in our SDKs.
# L402
Source: https://markdown2pdf.ai/l402
About L402 and how to use it
markdown2pdf.ai also accepts [X402](/x402) (USDC on Solana). L402 and X402 are
interchangeable — pick whichever rail your agent already has a wallet for.
## L402: The Missing Link in Internet Payment Infrastructure
*This text is adapted from the [L402 documentation](https://l402.org)*
In today's AI-driven world, L402 bridges the gap between automation and payments, enabling machine-friendly transactions where traditional human-centric payment flows fall short.
### Key Features
L402 simplifies and automates the process of handling payments on the internet, allowing seamless integration into digital workflows. It leverages HTTP as a foundation to standardize how payments are requested and processed, making it easier for AI agents and automated systems to interact with services.
* HTTP-based flow: simplifies payment handling by using HTTP 402 status codes and JSON payloads, allowing clients to request resources, pay and access them seamlessly.
* Standarize payment requests: services indicate payment requirements, making integration predictable and easier for developers to implement across different systems.
* Payment agnostic: works with a variety of payment solutions, from services like Stripe to cryptocurrencies, offering developers the flexibility to use the payment network that best fits their needs.
* Designed for automation: enables seamless, autonomous transactions between services and AI agents, removing the need for human intervention in payment processes.
* Extensible and open source: built to be open and adaptable, making it easy to extend and integrate with future payment solutions and evolving technologies.
Introduction
L402 makes payments a core part of HTTP interactions by leveraging the HTTP 402 status code. Payments were largely an afterthought in the internet's original design, even though the HTTP protocol reserved the 402 "Payment Required" status code for future use. Instead, payment solutions evolved around human-centric processes and relied on networks like Visa and Mastercard, making manual checkout flows the standard. This worked well for traditional web interactions, where payments were either preconfigured or managed manually by users.
With the rise of autonomous systems and AI agents that discover and interact with services independently, the limitations of these human-centric payment flows are becoming clear. These agents interact primarily through APIs and bypass browser-based interfaces, revealing a need for a payment model that supports automated, real-time transactions without human setup.
L402 addresses this gap by making payments "machine-friendly" on the internet. Leveraging HTTP's 402 status code and JSON payloads, L402 standardizes how services request payments directly within HTTP interactions, enabling AI agents and automated systems to handle payments as naturally as data exchange. This transforms payments into a core, automated component of the web.
A short summary of the L402 flow is as follows:
The entire flow uses standard HTTP semantics, making it compatible with any HTTP client - browsers, API clients, or AI agents. While the API, payment gateway, and server are shown as separate entities, with self-custodial methods they could be implemented as the same entity.
## markdown2pdf.ai and L402
markdown2pdf.ai is built on top of the L402 protocol, allowing you to easily convert markdown content into PDF format. It uses the L402 protocol to handle payments in a machine-friendly way, enabling AI agents and automated systems to interact with the service without manual intervention.
If you like, you can use our [REST API](/api-reference) directly, or you can use our [Python SDK](/python) to simplify the process of converting markdown to PDF. The SDK handles the payment flow automatically, allowing you to focus on your application logic without worrying about payment details.
# Markdown Syntax
Source: https://markdown2pdf.ai/markdown
A full listing of all the markdown we support in our markdown to PDF converter.
# Titles
We support headings in the standard markdown format.
```markdown theme={null}
# Heading 1
## Heading 2
```
# Text Formatting
We support most markdown formatting. Simply add `**`, `_`, or `~` around text to format it.
| Style | How to write it | Result |
| ------------- | ----------------- | ----------------- |
| Bold | `**bold**` | **bold** |
| Italic | `_italic_` | *italic* |
| Strikethrough | `~strikethrough~` | ~~strikethrough~~ |
* You can also use ***bold and italic*** text.
* We do not support subscript/superscript text.
# Links
You can add a link by wrapping text in `[]()`. You would write `[link to google](https://google.com)` to [link to google](https://google.com).
# Lists
We support both ordered and unordered lists.
## Unordered Lists
Use the `-` character to create an unordered list. For example:
```markdown theme={null}
Quantum mechanics is a complex field, but some of its key concepts include:
- **Wave-Particle Duality**: Particles, such as electrons and photons, exhibit both wave-like and particle-like properties.
- **Quantization**: Certain properties, such as energy, are quantized, meaning they can only take on discrete values.
- **Uncertainty Principle**: Formulated by Werner Heisenberg, it states that certain pairs of physical properties, like position and momentum, cannot both be known to arbitrary precision.
- **Superposition**: A quantum system can exist in multiple states at once until it is measured.
- **Entanglement**: Particles can become entangled, meaning the state of one particle is directly related to the state of another, no matter the distance between them.
```
## Ordered Lists
Use numbers followed by a period to create an ordered list. For example:
```markdown theme={null}
Quantum mechanics has a rich history, with key milestones including:
1. **Max Planck (1900)**: Introduced the concept of quantization of energy.
2. **Albert Einstein (1905)**: Explained the photoelectric effect using the concept of photons.
3. **Niels Bohr (1913)**: Developed the Bohr model of the atom.
4. **Werner Heisenberg (1927)**: Formulated the uncertainty principle.
5. **Erwin Schrödinger (1926)**: Developed wave mechanics and the Schrödinger equation.
```
# Blockquotes
## Singleline
To create a blockquote, add a `>` in front of a paragraph.
> Dorothy followed her through many of the beautiful rooms in her castle.
## Multiline
To create a blockquote, add a `>` in front of each paragraph.
> Dorothy followed her through many of the beautiful rooms in her castle.
>
> The Witch bade her clean the pots and kettles and sweep the floor and keep the fire fed with wood.
# Emoji
Emoji are supported too, though since we render for print, these will appear in black and white rather than colour.
# Equations
Be sure to use the `$$` syntax to write multi-line equations in markdown. For example, you can write the quadratic formula like this:
```markdown theme={null}
The quadratic formula is:
$$
x = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a}
$$
```
$$
x = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a}
$$
Here's a more advanced example mixing mixing the `$$` and `$` format, the latter can be used for in-line formatting:
```markdown theme={null}
$$
i\hbar \frac{\partial}{\partial t} \Psi(\mathbf{r}, t) = \hat{H} \Psi(\mathbf{r}, t)
$$
Where:
- $\Psi$ is the wave function of the system.
- $\hbar$ is the reduced Planck's constant.
- $\hat{H}$ is the Hamiltonian operator.
```
$$
i\hbar \frac{\partial}{\partial t} \Psi(\mathbf{r}, t) = \hat{H} \Psi(\mathbf{r}, t)
$$
Where:
* $\Psi$ is the wave function of the system.
* $\hbar$ is the reduced Planck's constant.
* $\hat{H}$ is the Hamiltonian operator.
# Images
You can add images using the `` syntax. For example, you can write ``.

You can control image sizing using this syntax:
```markdown theme={null}
{ width=20px height=20px }
```
# Horizontal lines
You can use the standard markdown syntax for horizontal lines, which is three dashes:
`---`
# Model Context Protocol
Source: https://markdown2pdf.ai/mcp-info
Easily use markdown2pdf.ai to convert markdown (.md) to high-quality and impactful PDF in your Model Context Protocol (MCP)-based agentic workflows using our MCP server.
# Getting started
[github.com/Serendipity-AI/markdown2pdf-mcp](https://github.com/Serendipity-AI/markdown2pdf-mcp)
## For Claude Desktop users
Get started using the pre-built markdown2pdf server in Claude for Desktop. You can find our [MCP server here](https://github.com/Serendipity-AI/markdown2pdf-mcp).
In this tutorial you will extend [Claude for Desktop](https://claude.ai/download) so that it can convert markdown to PDF using this service, prompting for payment when required. You can see how this looks below.
Don’t worry — it will ask you for your permission before executing these actions!
### 1. Download Claude for Desktop
First, start by downloading Claude for Desktop, choosing either macOS or Windows. (Linux is not yet supported for Claude for Desktop.)
Then follow the installation instructions.
If you already have Claude for Desktop, make sure it’s on the latest version by clicking on the Claude menu on your computer and selecting “Check for Updates…”
Because servers are locally run, MCP currently only supports desktop hosts. Remote hosts are in active development.
### 2. Add the markdown2pdf server
To add this functionality, we will be installing a pre-built markdown2pdf server in Claude for Desktop.
Get started by opening up the Claude menu on your computer and select “Settings…” Please note that these are not the Claude Account Settings found in the app window itself.
This is what it should look like on a Mac:
Click on “Developer” in the left-hand bar of the Settings pane, and then click on “Edit Config”:
This will create a configuration file at:
macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
Windows: `%APPDATA%\Claude\claude_desktop_config.json`
if you don’t already have one, and will display the file in your file system. Open up the configuration file in any text editor. Replace the file contents with this:
```json theme={null}
{
"mcpServers": {
"filesystem": {
"command": "npx",
"args": [
"-y",
"@serendipityai/markdown2pdf-mcp"
]
}
}
}
```
```json theme={null}
{
"mcpServers": {
"filesystem": {
"command": "npx",
"args": [
"-y",
"@serendipityai/markdown2pdf-mcp"
]
}
}
}
```
You will also need Node.js on your computer for this to run properly. To verify you have Node installed, open the command line on your computer.
On macOS, open the Terminal from your Applications folder
On Windows, press Windows + R, type “cmd”, and press Enter
Once in the command line, verify you have Node installed by entering in the following command:
```bash theme={null}
node --version
```
If you get an error saying “command not found” or “node is not recognized”, download Node from [nodejs.org](https://nodejs.org).
How does the configuration file work?
This configuration file tells Claude for Desktop which MCP servers to start up every time you start the application. In this case, we have added one server called “markdown2pdf” that will use the Node npx command to install and run @modelcontextprotocol/markdown2pdf. This server, described here, will let you convert markdown to PDF.
Command Privileges
Claude for Desktop will run the commands in the configuration file with the permissions of your user account, and access to your local files. Only add commands if you understand and trust the source.
### 3. Restart Claude
After updating your configuration file, you need to restart Claude for Desktop.
Upon restarting, you should see a settings icon in the bottom left corner of the input box:
After clicking on the settings icon, you should see the tools that come with the markdown2pdf server.
### 4. Try it out!
You can now talk to Claude and ask it to convert any markdown content in to PDF format. It should know when to call the relevant tools and facilitate any payments required. As needed, Claude will call the relevant tools and seek your approval before taking an action.
# Python SDK
Source: https://markdown2pdf.ai/python
Easily use markdown2pdf.ai to convert markdown (.md) to high-quality and impactful PDF in your Python-based agentic workflows using our Python software development kit (SDK).
# Getting started
[github.com/Serendipity-AI/markdown2pdf-python](https://github.com/Serendipity-AI/markdown2pdf-python)
Our [Python SDK](https://github.com/Serendipity-AI/markdown2pdf-python) makes it easy to use markdown2pdf.ai in your agentic workflows. Whilst using our REST APIs is straightforward, there are some complexities to handle around polling for payment completion and document generation that the SDK avoids you needing to worry about.
First up, you'll need to install the SDK:
```bash theme={null}
pip install markdown2pdf-python
```
With that done, you can try some sample code:
```python theme={null}
from markdown2pdf import MarkdownPDF
def pay(offer):
print("⚡ Lightning payment required")
print(f"Amount: {offer['amount']} {offer['currency']}")
print(f"Description: {offer['description']}")
print(f"Invoice: {offer['payment_request']}")
input("Press Enter once paid...")
client = MarkdownPDF(on_payment_request=pay)
path = client.convert(markdown="# Insert your own markdown content here", title="My document title", download_path="output.pdf")
print("Saved PDF to:", path)
```
This code will convert the markdown content you provide into a PDF, prompting you to pay via Lightning Network if necessary. The `on_payment_request` function is called when payment is required, allowing you to handle the payment process in your application. There are several ways of handling payments automatically, such as [fewsats.com](https://fewsats.com) or [albyhub](https://albyhub.com) or using a Lightning wallet that supports automatic payments. In the example above, we simply print the payment details and wait for you to confirm payment manually. You can simply copy and paste the lightning invoice into your lightning wallet to pay.
# SDK Reference
### `class MarkdownPDF`
The main class for interacting with the markdown2pdf.ai service.
#### `constructor`
Used for initializing the MarkdownPDF client.
This function is called when a payment is required. It receives an `offer` object containing details about the payment, such as amount, currency, description, and the Lightning invoice. You can implement your own logic to handle payments here. This parameter is required because the service uses the L402 protocol, which requires a payment to be made before generating the PDF.
```python theme={null}
def pay(offer):
print("⚡ Lightning payment required")
print(f"Amount: {offer['amount']} {offer['currency']}")
print(f"Description: {offer['description']}")
print(f"Invoice: {offer['payment_request']}")
input("Press Enter once paid...")
client = MarkdownPDF(on_payment_request=pay)
```
```json theme={null}
offer = {
"offer_id": "abc",
"amount": 1,
"currency": "usd",
"description": "test payment",
"payment_context_token": "def",
"payment_request_url": "http://path.to/payment/request",
}
```
This parameter enables you to override the default API URL. This is typically used in development or testing environments, but for normal usage you won't need to touch it and it can be set to `None` or omitted.
#### `convert`
Converts the provided markdown content into a PDF document. This method handles the payment process automatically if required, and saves a PDF to the specified path or returns it as bytes.
A string containing the markdown content you want to convert to PDF.
A note on markdown hashing
markdown2pdf.ai uses a hashing mechanism to ensure that you won't get charge twice for creating the same PDF. Subsequent requests to convert the same markdown content will return the previously generated PDF without requiring a new payment.
Typically your markdown will come from the output of an LLM or AI agent. If you want to load some from a file, you can do it like this:
```python theme={null}
from markdown2pdf import MarkdownPDF
from pathlib import Path
def pay(offer):
print("⚡ Lightning payment required")
print(f"Amount: {offer['amount']} {offer['currency']}")
print(f"Description: {offer['description']}")
print(f"Invoice: {offer['payment_request']}")
input("Press Enter once paid...")
client = MarkdownPDF(api_url="https://qa.api.markdown2pdf.ai", on_payment_request=pay)
markdown = Path("examples/md/Song of the silent stars.md").read_text(encoding="utf-8")
url = client.convert(markdown=markdown, title="Song of the silent stars", date="5th June 2025" )
print("PDF URL:", url)
```
A string containing the date to use on the front cover. If not provided, the current date will be used.
A string containing the title to use on the front cover. If not provided, a default will be used.
A string containing the path to use for saving the PDF file. If not provided, the PDF will be returned as bytes.
For example:
```python theme={null}
path = client.convert(markdown="# Save this one", download_path="output.pdf")
print("Saved PDF to:", path)
```
Whether or not to return the PDF as bytes instead of saving it to a file. If set to `True`, the method will return the PDF content as a byte string. If set to `False`, it will save the PDF to the specified `download_path` or return the path if no path is provided.
For example:
```python theme={null}
pdf_bytes = client.convert(markdown="# Memory use case", return_bytes=True)
print(f"PDF size in memory: {len(pdf_bytes)} bytes")
```
# Automating payments
If you want to automate payments, you can use a service like [fewsats.com](https://fewsats.com) or [Albyhub](https://albyhub.com) for Lightning, or any Solana wallet that holds USDC for X402. These services let you pay programmatically, completely headlessly.
```python theme={null}
from markdown2pdf import MarkdownPDF
from dotenv import load_dotenv
from pyalby import Account, Invoice, Payment
load_dotenv()
# In your .env file, ensure you have set the following. You can learn more about alby at https://albyhub.com.
# BASE_URL = https://api.getalby.com
# ALBY_ACCESS_TOKEN =
# LOG_LEVEL = INF
def pay(offer):
print("Paying using Alby:")
payment = Payment()
pay = payment.bolt11_payment(offer["payment_request"])
print(f"Payment made: {pay}")
client = MarkdownPDF(on_payment_request=pay)
path = client.convert(markdown="# Save this one using Alby", download_path="output.pdf") # Replace with your own unique markdown content to ensure you trigger L402.
print("Saved PDF to:", path)
```
```python theme={null}
from markdown2pdf import MarkdownPDF
import requests
LN_BITS_URL = "https://demo.lnbits.com/api/v1/payments"
ADMIN_KEY = "" # Replace with your LNbits admin key, see https://lnbits.com
def pay(offer):
print("Paying using lnbits:")
headers = {"X-Api-Key": ADMIN_KEY, "Content-Type": "application/json"}
data = {
"out": True,
"bolt11": offer["payment_request"],
}
res = requests.post(LN_BITS_URL, json=data, headers=headers)
res.raise_for_status()
print("Payment status:", res)
client = MarkdownPDF(on_payment_request=pay)
path = client.convert(markdown="# Save this one using lnbits", download_path="output.pdf") # Replace with your own unique markdown content to ensure you trigger L402.
print("Saved PDF to:", path)
```
```python theme={null}
from markdown2pdf import MarkdownPDF
from dotenv import load_dotenv
from fewsats.core import *
load_dotenv()
fs = Fewsats()
# In your .env file, ensure you have set the following. You can learn more about fewsats at https://fewsats.com.
# FEWSATS_API_KEY =
def pay(offer):
print("Paying using Fewsats:")
r = fs.pay_lightning(invoice=offer["payment_request"], amount=offer["amount"], currency=offer["currency"], description=offer["description"])
print(f"Payment made: {r}")
client = MarkdownPDF(on_payment_request=pay)
path = client.convert(markdown="# Save this one using Fewsats", download_path="output.pdf") # Replace with your own unique markdown content to ensure you trigger L402.
print("Saved PDF to:", path)
```
X402 uses a different flow to L402 — instead of a callback that receives an invoice,
the signer constructs a base64 `X-PAYMENT` header that is attached to the request
automatically. The simplest integration is to bypass the L402 `on_payment_request`
path entirely and talk to the REST API with an x402-aware HTTP client:
```python theme={null}
import os
import asyncio
from solders.keypair import Keypair
from x402 import x402Client, prefer_network
from x402.http.clients.httpx import x402HttpxClient
from x402.mechanisms.svm import KeypairSigner
from x402.mechanisms.svm.exact import ExactSvmScheme
async def main():
# 1. Initialize signer from base58 private key
keypair = Keypair.from_base58_string(os.environ["SOLANA_SIGNER_KEY"])
signer = KeypairSigner(keypair)
# 2. Configure x402 client for Solana Mainnet
NETWORK = "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp"
x402 = x402Client()
x402.register(NETWORK, ExactSvmScheme(signer=signer))
x402.register_policy(prefer_network(NETWORK))
# 3. The async client handles the 402 → sign → retry loop
async with x402HttpxClient(x402, timeout=120.0) as client:
resp = await client.post(
"https://api.markdown2pdf.ai/markdown",
json={
"data": {"text_body": "# Save this one using X402"},
"options": {"document_name": "output.pdf"},
},
)
resp.raise_for_status()
print("Paid tx proof:", resp.headers.get("payment-response"))
print("Poll:", resp.json()["path"])
if __name__ == "__main__":
asyncio.run(main())
```
A future release of the SDK will expose a first-class `on_x402_payment_required`
callback so you can pick the rail from a single `MarkdownPDF` constructor. See the
[X402 page](/x402) for details.
# Using from your AI Agents
In our Python SDK we provide examples of how to use the markdown2pdf.ai service from your AI agents using the [langchain](https://langchain.com) framework.
# Quickstart
Source: https://markdown2pdf.ai/quickstart
Convert markdown to PDF with our high-quality engine.
Here's a few examples of how to use markdown2pdf.ai in your code.
```bash theme={null}
pip install markdown2pdf-python
```
```python theme={null}
from markdown2pdf import MarkdownPDF
def pay(offer):
print("⚡ Lightning payment required")
print(f"Amount: {offer['amount']} {offer['currency']}")
print(f"Description: {offer['description']}")
print(f"Invoice: {offer['payment_request']}")
input("Press Enter once paid...")
client = MarkdownPDF(on_payment_request=pay)
path = client.convert(markdown="# Hello from Python", title="My document title", download_path="output.pdf")
print("Saved PDF to:", path)
```
```bash theme={null}
npm install @serendipityai/markdown2pdf-typescript
```
```typescript theme={null}
import { convertMarkdownToPdf } from "@serendipityai/markdown2pdf-typescript";
import type { OfferDetails } from "@serendipityai/markdown2pdf-typescript";
async function pay(offer: OfferDetails) {
console.log("⚡ Lightning payment required");
console.log(`Amount: ${offer.amount} ${offer.currency}`);
console.log(`Description: ${offer.description}`);
console.log(`Invoice: ${offer.payment_request}`);
await new Promise(resolve => { process.stdin.once("data", () => { resolve(); }); });
}
async function main() {
const result = await convertMarkdownToPdf("# Hello from Typescript", {
title: "My document title",
downloadPath: "output.pdf",
onPaymentRequest: pay
});
console.log("Saved PDF to:", result);
}
main().catch(console.error);
```
You can drop the below configuration in to your Claude MCP config to add PDF conversion to your chat workflow.
```bash theme={null}
"mcpServers": {
"markdown2pdf": {
"command":"npx",
"args": ["@serendipityai/markdown2pdf-mcp"],
"cwd": "~"
}
}
```
```bash theme={null}
pip install "x402[httpx]" solders
```
```python theme={null}
import os, asyncio
from solders.keypair import Keypair
from x402 import x402Client, prefer_network
from x402.http.clients.httpx import x402HttpxClient
from x402.mechanisms.svm import KeypairSigner
from x402.mechanisms.svm.exact import ExactSvmScheme
async def main():
keypair = Keypair.from_base58_string(os.environ["SOLANA_SIGNER_KEY"])
signer = KeypairSigner(keypair)
NETWORK = "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp"
x402 = x402Client()
x402.register(NETWORK, ExactSvmScheme(signer=signer))
x402.register_policy(prefer_network(NETWORK))
async with x402HttpxClient(x402, timeout=120.0) as client:
resp = await client.post(
"https://api.markdown2pdf.ai/markdown",
json={
"data": {"text_body": "# Hello from X402"},
"options": {"document_name": "hello.pdf"},
},
)
resp.raise_for_status()
print("Paid tx proof:", resp.headers.get("payment-response"))
print("Poll:", resp.json()["path"])
asyncio.run(main())
```
The `x402HttpxClient` handles the 402 → sign → retry loop for you. See the
[X402 page](/x402) for details and manual-flow variants.
You'll see that, in general, you pass your markdown content, a title and a download path to the `convert` function and it will handle the markdown conversion for you. A mandatory function must be provided which is used to handle the payment using a mechanism of your choice — either Lightning via [L402](/l402) or USDC on Solana via [X402](/x402). This could be a manual payment (copy-and-paste the Lightning invoice into a wallet of your choice, or sign an X402 `X-PAYMENT` header with a Solana wallet), but more likely will be handled programmatically via a Lightning wallet or a Solana signer.
# Typescript SDK
Source: https://markdown2pdf.ai/typescript
Easily use markdown2pdf.ai to convert markdown (.md) to high-quality and impactful PDF in your Typescript-based agentic workflows using our Typescript software development kit (SDK).
# Getting started
[github.com/Serendipity-AI/markdown2pdf-typescript](https://github.com/Serendipity-AI/markdown2pdf-typescript)
Our [Typescript SDK](https://github.com/Serendipity-AI/markdown2pdf-typescript) makes it easy to use markdown2pdf.ai in your agentic workflows. Whilst using our REST APIs is straightforward, there are some complexities (such as polling for payment completion and document generation) that the SDK handles for you.
First, install the SDK:
```bash theme={null}
npm install @serendipityai/markdown2pdf-typescript
```
If you run your TypeScript or ES module file (for example, via `node test.ts`) and see a warning like *Module type of file is not specified and it doesn't parse as CommonJS*, you can add a `"type": "module"` (or `"type": "commonjs"`) in your `package.json` (or rename your file to `.mjs`) to remove the warning.
With that done, you can try a sample code snippet:
```typescript theme={null}
import { convertMarkdownToPdf } from "@serendipityai/markdown2pdf-typescript";
import type { OfferDetails } from "@serendipityai/markdown2pdf-typescript";
async function pay(offer: OfferDetails) {
console.log("⚡ Lightning payment required");
console.log(`Amount: ${offer.amount} ${offer.currency}`);
console.log(`Description: ${offer.description}`);
console.log(`Invoice: ${offer.payment_request}`);
console.log("Press ENTER after completing the payment to continue...");
await new Promise< void >(resolve => { process.stdin.once("data", () => { resolve(); }); });
}
async function main() {
const result = await convertMarkdownToPdf("# Hello from Typescript", {
title: "My document title",
downloadPath: "output.pdf",
onPaymentRequest: pay
});
console.log("Saved PDF to:", result);
}
main().catch(console.error);
```
This code converts your markdown into a PDF. If a Lightning payment is required, the SDK calls your `onPaymentRequest` callback (here our `pay` function) so that you can handle the payment (for example, by printing the invoice and waiting for manual confirmation). You can also integrate with services like [fewsats.com](https://fewsats.com) or [albyhub](https://albyhub.com) for automatic payments.
***
# SDK Reference
## Types
The SDK exports the following types (and classes) (from "@serendipityai/markdown2pdf-typescript"):
A TypeScript type that describes the payment offer details. This type is used in the `onPaymentRequest` callback.
```typescript theme={null}
type OfferDetails = {
offer_id: string; // Unique identifier for the offer
amount: number; // The payment amount
currency: string; // The currency code (e.g., "usd")
description: string; // Description of what the payment is for
payment_context_token: string; // Token used for payment verification
payment_request_url: string; // URL where the payment request can be viewed
payment_request?: string; // Optional Lightning payment request (BOLT11 invoice)
};
```
Example usage:
```typescript theme={null}
async function pay(offer: OfferDetails) {
console.log(`Payment required: ${offer.amount} ${offer.currency}`);
console.log(`Description: ${offer.description}`);
if (offer.payment_request) {
console.log(`Lightning invoice: ${offer.payment_request}`);
}
// ... handle payment ...
}
```
A TypeScript type that describes the configuration options for the `convertMarkdownToPdf` function.
```typescript theme={null}
type ConvertToPdfParams = {
onPaymentRequest?: (offer: OfferDetails) => Promise; // Callback for handling Lightning payments
date?: string; // Date to display on the cover page
title?: string; // Title to display on the cover page
downloadPath?: string; // Path where the PDF should be saved
returnBytes?: boolean; // Whether to return the PDF as a Buffer instead of saving to file
apiUrl?: string; // Optional override for the API URL (for development/testing)
};
```
Example usage:
```typescript theme={null}
const options: ConvertToPdfParams = {
title: "My Document",
date: "2024-03-20",
downloadPath: "output.pdf",
onPaymentRequest: async (offer) => {
console.log(`Payment required: ${offer.amount} ${offer.currency}`);
// ... handle payment ...
}
};
const result = await convertMarkdownToPdf("# Hello World", options);
```
* **Markdown2PdfError** – A class (extending Error) thrown (or used) for general conversion errors.
* **PaymentRequiredError** – A class (extending Markdown2PdfError) thrown (or used) if a payment is required but no "onPaymentRequest" callback is provided.
***
## Function: convertMarkdownToPdf
**Signature:**
```typescript theme={null}
async function convertMarkdownToPdf(
markdown: string,
options?: ConvertToPdfParams
): Promise
```
**Description:**
Converts the provided markdown (a string) into a PDF. The function takes two parameters:
1. `markdown`: The markdown content to convert
2. `options`: An optional object containing configuration parameters for the conversion process
If a Lightning payment is required, the SDK calls your `onPaymentRequest` callback so that you can handle the payment. If `onPaymentRequest` is omitted and a payment is required, a `PaymentRequiredError` is thrown.\
The SDK retrieves the final PDF; either saving it to `downloadPath` or returning it as a `Buffer` if `returnBytes` is `true`.
**Parameters:**
A string containing the markdown content you want to convert to PDF.
A note on markdown hashing
markdown2pdf.ai uses a hashing mechanism to ensure that you won't get charged twice for creating the same PDF. Subsequent requests to convert the same markdown content will return the previously generated PDF without requiring a new payment.
For example:
```typescript theme={null}
const result = await convertMarkdownToPdf("# Hello from Typescript", {
title: "My document title",
downloadPath: "output.pdf"
});
```
A callback function that is called when a Lightning payment is required. The function receives an `offer` object containing payment details. This parameter is required because the service uses the L402 protocol, which requires a payment to be made before generating the PDF.
```typescript theme={null}
async function pay(offer: OfferDetails) {
console.log("⚡ Lightning payment required");
console.log(`Amount: ${offer.amount} ${offer.currency}`);
console.log(`Description: ${offer.description}`);
console.log(`Invoice: ${offer.payment_request}`);
await new Promise(resolve => { process.stdin.once("data", () => { resolve(); }); });
}
const result = await convertMarkdownToPdf("# Hello from Typescript", {
onPaymentRequest: pay,
downloadPath: "output.pdf"
});
```
```typescript theme={null}
offer = {
offer_id: "abc",
amount: 1,
currency: "usd",
description: "test payment",
payment_context_token: "def",
payment_request_url: "http://path.to/payment/request",
payment_request: "lnbc..." // Optional
}
```
A string containing the date to use on the front cover. If not provided, the current date will be used.
For example:
```typescript theme={null}
const result = await convertMarkdownToPdf("# Hello from Typescript", {
date: "5th June 2025",
downloadPath: "output.pdf"
});
```
A string containing the title to use on the front cover. If not provided, a default will be used.
For example:
```typescript theme={null}
const result = await convertMarkdownToPdf("# Hello from Typescript", {
title: "My document title",
downloadPath: "output.pdf"
});
```
A string containing the path to use for saving the PDF file. If not provided and returnBytes is false, the PDF URL will be returned instead.
For example:
```typescript theme={null}
const path = await convertMarkdownToPdf("# Save this one", {
downloadPath: "output.pdf"
});
console.log("Saved PDF to:", path);
```
Whether or not to return the PDF as bytes instead of saving it to a file. If set to `true`, the method will return the PDF content as a Buffer. If set to `false`, it will save the PDF to the specified `downloadPath` or return the URL if no path is provided.
For example:
```typescript theme={null}
const pdfBytes = await convertMarkdownToPdf("# Memory use case", {
returnBytes: true
});
console.log(`PDF size in memory: ${pdfBytes.length} bytes`);
```
This parameter enables you to override the default API URL. This is typically used in development or testing environments, but for normal usage you won't need to touch it and it can be set to `undefined` or omitted.
**Returns**
| Condition | Resolves to | Type |
| ----------------------------------------------------------------------- | --------------------------- | --------------------------------------------- |
| `downloadPath` is provided and `returnBytes` is `false` | The value of `downloadPath` | string |
| `returnBytes` is `true` | The PDF as a Buffer | Buffer |
| Neither `downloadPath` nor `returnBytes` is provided/true | The final download URL | string |
| An error occurs (network, timeout, or payment required and no callback) | Rejects with error | `Markdown2PdfError` or `PaymentRequiredError` |
The function always returns a `Promise` that resolves or rejects as described above.
***
# Automating payments
If you want to automate Lightning payments (so that your "onPaymentRequest" callback pays the invoice automatically), you can integrate with a service (or Lightning wallet) such as [fewsats.com](https://fewsats.com) or [albyhub](https://albyhub.com). For X402 (USDC on Solana) use any Solana wallet that holds USDC — see the X402 tab below.
```typescript theme={null}
import { convertMarkdownToPdf } from "@serendipityai/markdown2pdf-typescript";
import axios from 'axios';
import type { OfferDetails } from '@serendipityai/markdown2pdf-typescript';
// Configure Alby client
const ALBY_API_URL = 'https://api.getalby.com';
const client = axios.create({
baseURL: ALBY_API_URL,
headers: {
'Authorization': `Bearer ${process.env.ALBY_ACCESS_TOKEN}`,
'Content-Type': 'application/json'
}
});
async function pay(offer: OfferDetails) {
console.log("Paying using Alby:");
try {
// Pay the invoice using Alby's API
const response = await client.post('/payments/bolt11', {
invoice: offer.payment_request
});
if (response.status === 200)
console.log('Payment successful:', response.data);
else
console.log('Payment status:', response.status);
} catch (error) {
if (axios.isAxiosError(error))
console.error('Payment failed:', error.response?.data || error.message);
else
console.error('Payment failed:', error);
}
await new Promise(resolve => { process.stdin.once("data", () => resolve(undefined)); });
}
async function main() {
const path = await convertMarkdownToPdf("# Save this one using Alby", {
downloadPath: "output.pdf",
onPaymentRequest: pay
});
console.log("Saved PDF to:", path);
}
main().catch(console.error);
```
```typescript theme={null}
import { convertMarkdownToPdf } from "@serendipityai/markdown2pdf-typescript";
import axios from "axios";
import type { OfferDetails } from '@serendipityai/markdown2pdf-typescript';
// Constants
const LN_BITS_URL = "https://demo.lnbits.com/api/v1/payments";
const ADMIN_KEY = process.env.LNBITS_ADMIN_KEY as string; // Set in your env
async function pay(offer: OfferDetails) {
console.log("Paying using lnbits:");
try {
const response = await axios.post(
LN_BITS_URL,
{
out: true,
bolt11: offer.payment_request
},
{
headers: {
"X-Api-Key": ADMIN_KEY,
"Content-Type": "application/json"
}
}
);
console.log("Payment successful:", response.data);
} catch (error) {
if (axios.isAxiosError(error)) {
console.error("Payment failed:", error.response?.data || error.message);
} else {
console.error("Unexpected error:", error);
}
}
}
async function main() {
const path = await convertMarkdownToPdf("# Save this one using LNbits", {
downloadPath: "output.pdf",
onPaymentRequest: pay
});
console.log("Saved PDF to:", path);
}
main().catch(console.error);
```
```typescript theme={null}
import { convertMarkdownToPdf } from "@serendipityai/markdown2pdf-typescript";
import type { OfferDetails } from '@serendipityai/markdown2pdf-typescript';
import { Fewsats } from 'fewsats';
// Configure the SDK
const client = new Fewsats({ apiKey: process.env.FEWSATS_API_KEY });
async function pay(offer: OfferDetails) {
console.log("Paying using Fewsats:");
console.log(offer);
try {
// Use the SDK to pay the offer
const response = await client.payLightning(offer.payment_request, offer.amount, offer.currency, offer.description);
if (response.success) {
console.log('Payment successful:', response);
} else {
console.log('Payment failed:', response.error);
}
} catch (error) {
console.error('Payment failed:', error.message);
}
await new Promise(resolve => { process.stdin.once("data", () => resolve(undefined)); });
}
async function main() {
const path = await convertMarkdownToPdf("# Save this one using Fewsats", {
downloadPath: "output.pdf",
onPaymentRequest: pay
});
console.log("Saved PDF to:", path);
}
main().catch(console.error);
```
```typescript theme={null}
import axios from "axios";
import { withPaymentInterceptor } from "x402-axios";
import { Keypair } from "@solana/web3.js";
import { createKeypairSignerFromBytes } from "@solana/signers";
import bs58 from "bs58";
// Any Solana wallet that holds USDC
const secretKey = bs58.decode(process.env.SOLANA_SIGNER_KEY as string);
const keypair = Keypair.fromSecretKey(secretKey);
const signer = await createKeypairSignerFromBytes(keypair.secretKey);
const http = withPaymentInterceptor(axios.create(), signer);
async function main() {
const resp = await http.post("https://api.markdown2pdf.ai/markdown", {
data: { text_body: "# Save this one using X402" },
options: { document_name: "output.pdf" },
});
console.log("Paid tx proof:", resp.headers["x-payment-response"]);
console.log("Poll:", resp.data.path);
}
main().catch(console.error);
```
`x402-axios` intercepts the 402, signs an `X-PAYMENT` payload with your Solana
keypair, and retries automatically. A future SDK release will expose a first-class
`onX402PaymentRequired` option on `convertMarkdownToPdf`. See the
[X402 page](/x402) for protocol details.
***
# X402
Source: https://markdown2pdf.ai/x402
About X402 (Coinbase) and how to use it with markdown2pdf.ai
## X402: HTTP-native stablecoin payments
X402 is an open payment protocol from Coinbase that reactivates the HTTP `402 Payment Required`
status code for machine-to-machine stablecoin payments. It is complementary to
[L402](/l402) — same goal of programmable, accountless payments inside HTTP — just settling in
USDC on Solana instead of sats on the Lightning Network.
### Key properties
* **Stablecoin-denominated.** Payments settle in USDC on Solana, so the amount a
client sees in the 402 response is the exact number of dollars they will spend. No FX.
* **On-chain and low-friction.** Solana settlement is sub-second and costs a fraction
of a cent in gas. No counterparty risk after the facilitator returns success.
* **Header-driven.** The client attaches one base64 `X-PAYMENT` header to the original
request and re-submits. The server verifies + settles via a facilitator and returns
`200` with an `X-Payment-Response` header carrying the settlement proof.
* **Agent-friendly.** The flow is the same for humans and autonomous agents, and it's
supported out of the box by the Coinbase SDK as well as the open `x402-axios`, `x402-fetch`,
and Python `x402[httpx]` clients. Learn more at [x402.org](https://x402.org).
## markdown2pdf.ai and X402
markdown2pdf.ai accepts both L402 (Lightning) and X402 (USDC on Solana). Pick whichever rail
your agent already has a wallet for.
* **L402:** 5 sats per PDF, instant via Lightning.
* **X402:** \$0.01 USDC per PDF, instant via Solana.
You pay per document. Re-submitting the same markdown reuses the paid receipt — our server
dedupes on a `sha256` of the markdown body for both rails.
## Flow
If you haven't paid yet, the server returns `402` with a body that contains an `offers[]`
array. When X402 is enabled there are two entries — one for Lightning and one for X402.
The X402 offer carries `payment_methods: ["x402"]` and a `metadata.x402` block with the
native x402 `accepts[]` array — the payment requirements the facilitator will validate
against (network, asset, `payTo`, `maxAmountRequired`, scheme).
Use any x402-aware client (Coinbase SDK, `x402-axios`, `x402-fetch`, or Python
`x402[httpx]`) to construct and base64-encode a `PaymentPayload` authorising the transfer.
Send the exact same request body, plus `X-PAYMENT: ` as a header.
A successful response carries `X-Payment-Response: ` — the base64-encoded
`SettleResponse` with the on-chain transaction hash. Your agent can log this for
auditing, or ignore it and proceed straight to polling the job status.
**Alternative shortcut.** If your client already speaks x402 natively and does not want to
parse the L402 envelope, it can ignore the outer body entirely and read `accepts[]`
straight from `offers[].metadata.x402.accepts` — no second round-trip required. We also
expose a dedicated `POST /payment_request/x402` endpoint for clients that prefer an
explicit per-offer lookup.
## REST example
Python, using [`x402[httpx]`](https://github.com/coinbase/x402). Set `SOLANA_SIGNER_KEY`
to your wallet's base58 private key (64-byte keypair as exported by most Solana wallets).
```python theme={null}
import os
import asyncio
from solders.keypair import Keypair
from x402 import x402Client, prefer_network
from x402.http.clients.httpx import x402HttpxClient
from x402.mechanisms.svm import KeypairSigner
from x402.mechanisms.svm.exact import ExactSvmScheme
async def main():
# 1. Initialize signer from base58 private key
keypair = Keypair.from_base58_string(os.environ["SOLANA_SIGNER_KEY"])
signer = KeypairSigner(keypair)
# 2. Configure x402 client for Solana Mainnet
NETWORK = "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp"
x402 = x402Client()
x402.register(NETWORK, ExactSvmScheme(signer=signer))
x402.register_policy(prefer_network(NETWORK))
# 3. The async client handles the 402 → sign → retry loop
async with x402HttpxClient(x402, timeout=120.0) as client:
resp = await client.post(
"https://api.markdown2pdf.ai/markdown",
json={
"data": {"text_body": "# Hello from X402"},
"options": {"document_name": "hello.pdf"},
},
)
resp.raise_for_status()
print("Paid tx proof:", resp.headers.get("payment-response"))
print("Poll path:", resp.json()["path"])
if __name__ == "__main__":
asyncio.run(main())
```
The `x402HttpxClient` handles the 402 → sign → retry loop for you. If you want to do it
manually (e.g. to customise the flow), the recipe is:
```python theme={null}
import base64, json, httpx
from x402.http.utils import encode_payment_signature_header
# 1. First request returns 402
r = httpx.post(url, json=payload)
assert r.status_code == 402
accepts = r.json()["offers"][1]["metadata"]["x402"]["accepts"][0] # x402 offer
# 2. Build + sign a PaymentPayload using the scheme's client-side helper
# (see x402 docs for ExactSvmClientScheme.build_payment_payload)
payment_header = encode_payment_signature_header(signed_payload)
# 3. Re-POST with X-PAYMENT
r = httpx.post(url, json=payload, headers={"X-PAYMENT": payment_header})
r.raise_for_status()
```
## When to pick X402 vs L402
Your agent already holds sats and/or uses Lightning wallets such as Alby, LNbits, or
Fewsats. Lightning settlement is instant and fees are negligible. See the
[L402 page](/l402).
Your agent already holds USDC on Solana, or you want dollar-denominated predictability.
X402 is well-supported by the Coinbase CDP SDK and by `x402-axios` / `x402-fetch` on
the TypeScript side.
Either way, the document generation backend is identical — the choice is purely about how
you want to pay.