All tests run on an 8-year-old MacBook Air (Intel). My entire business infrastructure—payments, taxes, licensing—runs through a single platform, and the integration code fits in about 200 lines of Rust.
Being a great developer is only half of building a software business. You also need to handle global payments, tax compliance, and license management. When I started selling the Hiyoko Suite internationally, I needed a payment system that handled the complexity of VAT, GST, and sales tax in 100+ countries without requiring me to become a tax lawyer. LemonSqueezy's Merchant of Record model solved this completely.
TL;DR
- Merchant of Record (MoR) services like LemonSqueezy handle international tax compliance—you receive net payouts, they handle the rest.
- Per-app licensing with unique checkout links lets users buy only what they need.
- License validation happens via a simple API call on app startup, with offline grace periods for reliability.
- The checkout flow runs in a secure Tauri webview window—zero native payment UI code needed.
Why Merchant of Record Matters
If you sell software directly to 100+ countries, you're technically responsible for collecting and remitting VAT (EU), GST (Australia, India), sales tax (US states), and consumption tax (Japan). The compliance burden alone would consume my entire development time.
LemonSqueezy acts as the legal seller. They collect payment, handle all tax obligations, and send me a net payout. From a tax perspective, I have one "customer" (LemonSqueezy), not thousands of individual buyers across dozens of tax jurisdictions.
Per-App Checkout Links
Each Hiyoko app has its own LemonSqueezy checkout URL. This supports a per-app pricing model where users buy only the tools they need:
// License configuration — each app maps to its own checkout
pub struct AppLicense {
pub app_id: &'static str,
pub checkout_url: &'static str,
pub trial_days: u32,
}
const LICENSES: &[AppLicense] = &[
AppLicense {
app_id: "hiyoko-mtp",
checkout_url: "https://hiyokomtp.lemonsqueezy.com/checkout/buy/2e966b64-...",
trial_days: 14,
},
AppLicense {
app_id: "hiyoko-shot",
checkout_url: "https://hiyokomtp.lemonsqueezy.com/checkout/buy/6cd85b5d-...",
trial_days: 7,
},
// ... 8 more apps
];
HiyokoKit serves as the "All-in-One" bundle—a single purchase that unlocks the full suite at a discount. For users who only need screenshot transfers, HiyokoShot is available standalone for $7.
Checkout in a Tauri Webview
Integrating a payment flow into a desktop app doesn't require building native payment UI. I open the LemonSqueezy hosted checkout in a secure Tauri webview window:
use tauri::Manager;
#[tauri::command]
async fn open_checkout(app: tauri::AppHandle, checkout_url: String) -> Result<(), String> {
// Open checkout in a dedicated window
let _window = tauri::WebviewWindowBuilder::new(
&app,
"checkout",
tauri::WebviewUrl::External(checkout_url.parse().map_err(|e| format!("{e}"))?),
)
.title("Purchase — Hiyoko Suite")
.inner_size(480.0, 720.0)
.resizable(false)
.center()
.build()
.map_err(|e| e.to_string())?;
Ok(())
}
The checkout page is fully hosted by LemonSqueezy—responsive design, payment method selection, tax calculation, and receipt generation all happen on their end. My app just opens a window and waits for the result.
License Validation
After purchase, the user receives a license key. On app startup, the hiyoko-helper crate validates it:
use serde::Deserialize;
#[derive(Deserialize)]
struct LicenseResponse {
valid: bool,
license_key: LicenseKeyData,
}
#[derive(Deserialize)]
struct LicenseKeyData {
status: String, // "active", "expired", "disabled"
activation_limit: u32,
activations_count: u32,
}
/// Validate a license key against LemonSqueezy's API
async fn validate_license(key: &str) -> Result<LicenseState> {
let client = reqwest::Client::new();
let response: LicenseResponse = client
.post("https://api.lemonsqueezy.com/v1/licenses/validate")
.json(&serde_json::json!({
"license_key": key,
"instance_name": get_machine_id()?
}))
.send()
.await?
.json()
.await?;
match (response.valid, response.license_key.status.as_str()) {
(true, "active") => Ok(LicenseState::Active),
(_, "expired") => Ok(LicenseState::Expired),
_ => Ok(LicenseState::Invalid),
}
}
/// Startup check with offline grace period
pub fn check_license_on_startup(app_id: &str) -> Result<LicenseState> {
let stored = load_stored_license(app_id)?;
match validate_license_sync(&stored.key) {
Ok(state) => {
cache_validation_result(app_id, &state)?;
Ok(state)
}
Err(_) => {
// Network failure — use cached result with 7-day grace
let cached = load_cached_validation(app_id)?;
if cached.age_days() < 7 {
Ok(cached.state)
} else {
Ok(LicenseState::RequiresOnlineCheck)
}
}
}
}
The actual implementation handles additional edge cases not shown here.
The offline grace period is critical. My users are mobile developers who might be on a plane, in a subway, or in a building with no internet. Requiring a network connection just to open a file transfer tool would be hostile UX. The 7-day cache means the app works reliably offline while still preventing license abuse.
Why This Works for Solo Devs
The entire business stack—checkout, licensing, tax compliance, receipt generation—is handled by LemonSqueezy plus about 200 lines of Rust in hiyoko-helper. I don't run a payment server. I don't manage a customer database. I don't file VAT returns in 27 EU member states.
My 8-year-old MacBook Air doesn't need to be a commerce platform. It just needs to build great software and call a simple API. Focus on your code, let the platforms handle the complexity.
What payment/licensing setup do you use for your indie projects? Has anyone tried the Merchant of Record model versus handling taxes yourself?
If this was helpful, check out HiyokoKit — all-in-one Android dev toolkit (MTP file manager, Logcat+AI, ADB Tools, Monitor).
Built with Rust + Tauri v2. Tested on an 8-year-old MacBook Air.
Top comments (0)